More work on the massic refactor to functional react

This commit is contained in:
Colin Dawson 2026-01-30 09:44:21 +00:00
parent 471e239591
commit 8308515c9b
34 changed files with 2239 additions and 1681 deletions

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"chat.tools.terminal.autoApprove": {
"npx tsc": true
}
}

View File

@ -4,27 +4,50 @@
"AuditLogs": "Audit Logs", "AuditLogs": "Audit Logs",
"BlockedIPAddresses": "Blocked IP addresses", "BlockedIPAddresses": "Blocked IP addresses",
"BlockedIPs": "Blocked IPs", "BlockedIPs": "Blocked IPs",
"Changes": "Changes",
"ClientDomainManager": "Client Domain Manager", "ClientDomainManager": "Client Domain Manager",
"ClientDomains": "Client Domains", "ClientDomains": "Client Domains",
"Comment": "Comment",
"CustomFieldManager": "Custom Field Manager", "CustomFieldManager": "Custom Field Manager",
"CustomFields": "Custom Fields", "CustomFields": "Custom Fields",
"DisplayName": "Display Name",
"EntityDisplayName": "Entity Display Name",
"e-print": "e-print", "e-print": "e-print",
"e-suite": "e-suite", "e-suite": "e-suite",
"ErrorLogs": "Error Logs", "ErrorLogs": "Error Logs",
"ExceptionJson": "Exception JSON",
"ExceptionLogs": "Exception Logs", "ExceptionLogs": "Exception Logs",
"Forms": "Forms", "Forms": "Forms",
"FormTemplateManager": "Form Template Manager", "FormTemplateManager": "Form Template Manager",
"Glossaries": "Glossaries", "Glossaries": "Glossaries",
"GlossaryManager": "Glossary Manager", "GlossaryManager": "Glossary Manager",
"Home": "Home", "Home": "Home",
"Id": "Id",
"Application": "Application",
"Message": "Message",
"ShowJSON": "Show JSON",
"ShowStackTrace": "Show stack trace",
"OccuredAt": "Occured At",
"IPAddress": "IP Address",
"IPAddressUnblocked": "IP Address '{{ip}}' unblocked.",
"Loading": "Loading",
"Name": "Name",
"NumberOfAttempts": "Number Of Attempts",
"NewValue": "New Value",
"OldValue": "Old Value",
"PressAgainToUnblock": "Press again to unblock",
"Sequence": "Sequence", "Sequence": "Sequence",
"SequenceManager": "Sequence Manager", "SequenceManager": "Sequence Manager",
"SiteManager": "Site Manager", "SiteManager": "Site Manager",
"SpecificationManager": "Specification Manager", "SpecificationManager": "Specification Manager",
"StackTrace": "Stack Trace",
"SsoManager": "Sso Manager", "SsoManager": "Sso Manager",
"Support": "Support", "Support": "Support",
"SupportingData": "Supporting Data",
"Timing": "Timing",
"Type": "Type",
"UnblockedInMinutes": "Unblocked In (Minutes)",
"UserManager": "User Manager", "UserManager": "User Manager",
"Users": "Users", "UserName": "User Name",
"Name": "Name", "Users": "Users"
"Loading": "Loading"
} }

View File

@ -1,30 +0,0 @@
{
"Admin": "Admin (In french)",
"AuditLog": "Audit Logs (In french)",
"AuditLogs": "Audit Logs (In french)",
"BlockedIPAddresses": "Blocked IP addresses (In french)",
"BlockedIPs": "Blocked IPs (In french)",
"ClientDomainManager": "Client Domain Manager (In french)",
"ClientDomains": "Client Domains (In french)",
"CustomFieldManager": "Custom Field Manager (In french)",
"CustomFields": "Custom Fields (In french)",
"e-print": "e-print (In french)",
"e-suite": "e-suite (In french)",
"ErrorLogs": "Error Logs (In french)",
"ExceptionLogs": "Exception Logs (In french)",
"Forms": "Forms (In french)",
"FormTemplateManager": "Form Template Manager (In french)",
"Glossaries": "Glossaries (In french)",
"GlossaryManager": "Glossary Manager (In french)",
"Home": "Home (In french)",
"Sequence": "Sequence (In french)",
"SequenceManager": "Sequence Manager (In french)",
"SiteManager": "Site Manager (In french)",
"SpecificationManager": "Specification Manager (In french)",
"SsoManager": "Sso Manager (In french)",
"Support": "Support (In french)",
"UserManager": "User Manager (In french)",
"Users": "Users (In french)",
"Name": "Name (In french)",
"Loading": "Loading (In french)"
}

26
src/Sass/_errorLogs.scss Normal file
View File

@ -0,0 +1,26 @@
.errorlogs-pre {
white-space: pre;
overflow-x: auto;
font-family: Menlo, Monaco, "Courier New", monospace;
background: transparent; /* removed fixed white background to respect app theme */
border: 1px solid transparent; /* keep layout without forcing a light border color */
padding: 0.5rem;
border-radius: 0.25rem;
margin: 0;
font-size: 0.9rem;
}
.errorlogs-ExceptionJson {
max-width: 800px;
max-height: 600px;
}
.errorlogs-StackTraceJson {
max-width: 500px;
max-height: 600px;
}
.errorlogs-SupportingData {
max-width: 500px;
max-height: 600px;
}

View File

@ -0,0 +1,63 @@
.expandable-cell {
display: block; // stack: button on top, content below
.expandable-cell__button {
border: none;
background: transparent;
padding: 0;
cursor: pointer;
color: inherit;
font-size: 1rem;
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.expandable-cell__title {
margin-right: 0.25rem;
font-size: 0.95rem;
line-height: 1;
}
.expandable-cell__content_defaults {
max-width: 600px;
max-height: 600px;
}
.expandable-cell__content {
margin-top: 0.5rem;
width: 100%;
overflow-x: auto; /* horizontal scrollbar always visible if needed */
overflow-y: auto; /* vertical scrollbar always visible if needed */
transition: height 240ms ease;
}
.expandable-cell__content-inner {
transition: opacity 180ms ease;
opacity: 1;
min-width: max-content; /* allow long lines to trigger horizontal scroll */
width: fit-content;
/* Remove overflow properties from here */
height: 100%;
}
.expandable-cell__icon {
display: inline-block;
transform-origin: center;
transition: transform 180ms ease;
transform: rotate(0deg);
}
&.is-open {
.expandable-cell__icon {
transform: rotate(90deg);
}
}
&:not(.is-open) {
.expandable-cell__content-inner {
opacity: 0;
}
}
}

View File

@ -1,7 +1,7 @@
@import "../../node_modules/bootstrap/scss/functions"; @import "../../node_modules/bootstrap/scss/functions";
//default variable overrides //default variable overrides
@import './_esuiteVariables.scss'; @import "./_esuiteVariables.scss";
@import "../../node_modules/bootstrap/scss/variables"; @import "../../node_modules/bootstrap/scss/variables";
@import "../../node_modules/bootstrap/scss/variables-dark"; @import "../../node_modules/bootstrap/scss/variables-dark";
@ -26,6 +26,8 @@
@import "./pill.scss"; @import "./pill.scss";
@import "./multiSelect.scss"; @import "./multiSelect.scss";
@import "./horizionalTabs"; @import "./horizionalTabs";
@import "./_expandableCell.scss";
@import "./_errorLogs.scss";
//Changes needed to make MS Edge behave the same as other browsers //Changes needed to make MS Edge behave the same as other browsers
input::-ms-reveal { input::-ms-reveal {

View File

@ -0,0 +1,69 @@
import React, { useState, useRef } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
interface Props {
children: React.ReactNode;
defaultCollapsed?: boolean;
className?: string;
contentClassName?: string;
title?: React.ReactNode;
}
export default function ExpandableCell({
children,
defaultCollapsed = true,
className,
contentClassName = "expandable-cell__content_defaults",
title,
}: Props) {
const [open, setOpen] = useState(!defaultCollapsed);
const contentRef = useRef<HTMLDivElement | null>(null);
const contentHeight = contentRef.current
? contentRef.current.scrollHeight
: 0;
const calcMaxHeight = open ? `${contentHeight}px` : "0px";
const classes = ["expandable-cell", className, open ? "is-open" : ""]
.filter(Boolean)
.join(" ");
return (
<div className={classes}>
<button
type="button"
aria-expanded={open}
onClick={() => setOpen((s) => !s)}
className="expandable-cell__button"
aria-label={
typeof title === "string"
? `${open ? "Hide" : "Show"} ${title}`
: undefined
}
>
{title ? <span className="expandable-cell__title">{title}</span> : null}
<FontAwesomeIcon
className="expandable-cell__icon"
icon={faChevronRight}
/>
</button>
<div
className={["expandable-cell__content", contentClassName]
.filter(Boolean)
.join(" ")}
style={{ height: calcMaxHeight }}
aria-hidden={!open}
>
<div
ref={contentRef}
className="expandable-cell__content-inner"
style={{ height: "100%" }}
>
{children}
</div>
</div>
</div>
);
}

View File

@ -1,109 +1,119 @@
import React from "react"; import React, { useCallback } from "react";
import { faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight } from "@fortawesome/free-solid-svg-icons"; import {
faAngleDoubleLeft,
faAngleLeft,
faAngleRight,
faAngleDoubleRight,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Paginated } from "../../services/Paginated"; import { Paginated } from "../../services/Paginated";
import Button, { ButtonType } from "./Button"; import Button, { ButtonType } from "./Button";
import { InputType } from "./Input"; import { InputType } from "./Input";
interface PaginationProps<T> { interface PaginationProps<T> {
data : Paginated<T> data: Paginated<T> | null;
onChangePage: (page: number, pageSize: number) => void; onChangePage: (page: number, pageSize: number) => void;
onUnselect?: () => void; onUnselect?: () => void;
} }
interface PaginationState { function Pagination<T>({
data,
} onChangePage,
onUnselect,
class Pagination<T> extends React.Component<PaginationProps<T>, PaginationState> { }: PaginationProps<T>): JSX.Element {
state = { } const changePage = useCallback(
(page: number, pageSize: number) => {
changePage( page : number, pageSize : number ){
const {onChangePage, onUnselect} = this.props;
onChangePage(page, pageSize); onChangePage(page, pageSize);
if (onUnselect) { if (onUnselect) onUnselect();
onUnselect(); },
} [onChangePage, onUnselect],
} );
clickFirst = () => { if (data === null) return <></>;
const { pageSize } = this.props.data;
this.changePage(1, pageSize ); const { page, pageSize, totalPages, count } = data;
}
clickPrevious = () => { const clickFirst = () => changePage(1, pageSize);
const { page, pageSize } = this.props.data; const clickPrevious = () => changePage(page - 1, pageSize);
const clickNext = () => changePage(page + 1, pageSize);
const clickLast = () => changePage(totalPages, pageSize);
this.changePage(page - 1, pageSize); const PageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
} const newPageSize = Number(e.currentTarget.value);
changePage(page, newPageSize);
clickNext = () => { };
const { page, pageSize } = this.props.data;
this.changePage(page + 1, pageSize);
}
clickLast = () => {
const { totalPages, pageSize } = this.props.data;
this.changePage(totalPages, pageSize);
}
PageSizeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const input = e.currentTarget;
const { page } = this.props.data;
let newPageSize : number = +input.value;
this.changePage(page, newPageSize);
}
handlePageSelect = ( e : React.ChangeEvent<HTMLInputElement>) =>
{
const { pageSize, totalPages } = this.props.data;
const input = e.currentTarget;
const newPage = Number(input.value);
const handlePageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const newPage = Number(e.currentTarget.value);
if (1 <= newPage && newPage <= totalPages) { if (1 <= newPage && newPage <= totalPages) {
this.changePage(newPage, pageSize); changePage(newPage, pageSize);
}
} }
};
render() { const pageSizeOptions = [10, 25, 50, 100];
const { data } = this.props; return (
<div className="d-flex py-2 bg-body-tertiary pagination">
if (data === null)
return <></>
const pageSizeOptions = [{ _id: "10", name: "10" },
{ _id: "25", name: "25" },
{ _id: "50", name: "50" },
{ _id: "100", name: "100" }];
return ( <div className="d-flex py-2 bg-body-tertiary pagination">
<span className="px-2"> <span className="px-2">
<select value={data.pageSize} className="form-select" onChange={this.PageSizeChange}> <select
{pageSizeOptions.map(({ _id, name }) => ( value={pageSize}
<option key={_id} value={_id}> className="form-select"
{name} onChange={PageSizeChange}
>
{pageSizeOptions.map((n) => (
<option key={n} value={n}>
{n}
</option> </option>
))} ))}
</select> </select>
</span> </span>
<span> <span>
<Button className="me-1" buttonType={ButtonType.primary} onClick={this.clickFirst} disabled={data.page < 2}><FontAwesomeIcon icon={faAngleDoubleLeft}/></Button> <Button
<Button className="me-1"buttonType={ButtonType.primary} onClick={this.clickPrevious} disabled={data.page < 2}><FontAwesomeIcon icon={faAngleLeft}/></Button> className="me-1"
<span className="me-1"><input type={InputType.number} value={data.page} min={1} max={data.totalPages} onChange={this.handlePageSelect}/> of {data.totalPages}</span> buttonType={ButtonType.primary}
<Button className="me-1" buttonType={ButtonType.primary} onClick={this.clickNext} disabled={data.page >= data.totalPages}><FontAwesomeIcon icon={faAngleRight}/></Button> onClick={clickFirst}
<Button className="me-1" buttonType={ButtonType.primary} onClick={this.clickLast} disabled={data.page >= data.totalPages}><FontAwesomeIcon icon={faAngleDoubleRight}/></Button> disabled={page < 2}
<span className="me-1">{data.count} Items</span> >
<FontAwesomeIcon icon={faAngleDoubleLeft} />
</Button>
<Button
className="me-1"
buttonType={ButtonType.primary}
onClick={clickPrevious}
disabled={page < 2}
>
<FontAwesomeIcon icon={faAngleLeft} />
</Button>
<span className="me-1">
<input
type={InputType.number}
value={page}
min={1}
max={totalPages}
onChange={handlePageSelect}
/>{" "}
of {totalPages}
</span> </span>
</div>); <Button
} className="me-1"
buttonType={ButtonType.primary}
onClick={clickNext}
disabled={page >= totalPages}
>
<FontAwesomeIcon icon={faAngleRight} />
</Button>
<Button
className="me-1"
buttonType={ButtonType.primary}
onClick={clickLast}
disabled={page >= totalPages}
>
<FontAwesomeIcon icon={faAngleDoubleRight} />
</Button>
<span className="me-1">{count} Items</span>
</span>
</div>
);
} }
export default Pagination; export default Pagination;

View File

@ -1,22 +1,16 @@
import React from "react"; import React from "react";
import authenticationService from "../../modules/frame/services/authenticationService"; import authenticationService from "../../modules/frame/services/authenticationService";
interface PermissionProps { interface PermissionProps {
privilegeKey: string; privilegeKey: string;
children: React.ReactNode; children: React.ReactNode;
} }
class Permission extends React.Component<PermissionProps> { const Permission: React.FC<PermissionProps> = ({ privilegeKey, children }) => {
render() {
const { privilegeKey, children } = this.props;
const hasAccess = authenticationService.hasAccess(privilegeKey); const hasAccess = authenticationService.hasAccess(privilegeKey);
if (hasAccess === false) if (!hasAccess) return null;
return ( <></> ); return <>{children}</>;
else };
return ( <>{children}</> );
}
}
export default Permission; export default Permission;

View File

@ -1,21 +1,13 @@
import React from "react"; import { useEffect } from "react";
interface RedirectProps { interface RedirectProps {
to : string to: string;
} }
interface RedirectState { export default function Redirect({ to }: RedirectProps) {
useEffect(() => {
}
class Redirect extends React.Component<RedirectProps, RedirectState> {
render() {
const {to} = this.props;
window.location.replace(to); window.location.replace(to);
}, [to]);
return null; return null;
} }
}
export default Redirect;

View File

@ -1,43 +1,66 @@
import React from "react"; import React from "react";
import Option from "./option"; import Option from "./option";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../i18n/i18n";
export interface SelectProps { export interface SelectProps {
includeLabel? : boolean, includeLabel?: boolean;
name : string, name: string;
label : string, label: string;
error? : string, error?: string;
value : unknown value: unknown;
options? : Option[], options?: Option[];
includeBlankFirstEntry? : boolean, includeBlankFirstEntry?: boolean;
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void; onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
} }
function GenerateValue( value : unknown ) function GenerateValue(value: unknown) {
{ if (value === true) return "true";
let actualValue = value; if (value === false) return "false";
return value as string | number | readonly string[] | undefined;
if (value === true)
return "true";
if (value === false)
return "false";
return actualValue as string | number | readonly string[] | undefined;
} }
class Select extends React.Component<SelectProps> { export default function Select({
render() { includeLabel,
const { includeLabel, name, label, error, value, options, includeBlankFirstEntry, onChange, ...rest } = this.props; name,
label,
error,
value,
options,
includeBlankFirstEntry,
onChange,
...rest
}: SelectProps) {
const { t } = useTranslation<typeof Namespaces.Common>();
const actualValue = GenerateValue(value); const actualValue = GenerateValue(value);
return ( return (
<div className="form-group"> <div className="form-group">
{(includeLabel===undefined || includeLabel===true) && <label htmlFor={name}>{label}</label>} {(includeLabel === undefined || includeLabel === true) && (
{!options && <select multiple {...rest} id={name} value={actualValue} className="form-control loading" name={name} onChange={onChange}> <label htmlFor={name}>{label}</label>
<option value="loading..." /> )}
</select>} {!options && (
{options && <select
<select {...rest} id={name} value={actualValue} className="form-control" name={name} onChange={onChange}> multiple
{...rest}
id={name}
value={actualValue}
className="form-control loading"
name={name}
onChange={onChange}
>
<option value="loading...">{t("Loading")}...</option>
</select>
)}
{options && (
<select
{...rest}
id={name}
value={actualValue}
className="form-control"
name={name}
onChange={onChange}
>
{includeBlankFirstEntry && <option value="" />} {includeBlankFirstEntry && <option value="" />}
{options?.map(({ _id, name }) => ( {options?.map(({ _id, name }) => (
<option key={_id} value={_id}> <option key={_id} value={_id}>
@ -45,11 +68,8 @@ class Select extends React.Component<SelectProps> {
</option> </option>
))} ))}
</select> </select>
} )}
{error && <div className="alert alert-danger">{error}</div>} {error && <div className="alert alert-danger">{error}</div>}
</div> </div>
); );
} }
};
export default Select;

View File

@ -1,18 +1,10 @@
import React from 'react'; import React from "react";
interface TabProps { interface TabProps {
label: string; label: string;
children : JSX.Element | JSX.Element[]; children: React.ReactNode;
} }
class Tab extends React.Component<TabProps> { export default function Tab({ label, children }: TabProps): JSX.Element {
render() { return <div data-tab-label={label}>{children}</div>;
return (
<div>
</div>
);
} }
}
export default Tab;

View File

@ -1,36 +1,25 @@
import React from 'react'; import React, { useCallback } from "react";
interface TabHeaderProps { interface TabHeaderProps {
isActive : boolean, isActive: boolean;
label : string label: string;
onClick : any; onClick: (label: string) => void;
} }
class TabHeader extends React.Component<TabHeaderProps> { export default function TabHeader({
isActive,
onClick = () => { label,
const { label, onClick } = this.props;
onClick(label);
};
render() {
const {
onClick, onClick,
props: { isActive, label }, }: TabHeaderProps) {
} = this; const handleClick = useCallback(() => onClick(label), [onClick, label]);
let className = "tab-list-item"; const className = isActive
? "tab-list-item tab-list-active"
if (isActive === true) { : "tab-list-item";
className += " tab-list-active";
}
return ( return (
<li className={className} onClick={onClick}> <li className={className} onClick={handleClick}>
{label} {label}
</li> </li>
); );
} }
}
export default TabHeader;

View File

@ -1,14 +1,14 @@
import { Component } from 'react'; import React, { useEffect, useState } from "react";
import { Paginated } from '../../services/Paginated'; import { Paginated } from "../../services/Paginated";
import Column from './columns'; import Column from "./columns";
import TableBody, { AuditParams } from './TableBody'; import TableBody, { AuditParams } from "./TableBody";
import TableFooter from './TableFooter'; import TableFooter from "./TableFooter";
import TableHeader from './TableHeader'; import TableHeader from "./TableHeader";
import debounce from 'lodash.debounce'; import debounce from "lodash.debounce";
export interface PublishedTableProps<T> { export interface PublishedTableProps<T> {
data: Paginated<T>, data: Paginated<T>;
sortColumn? : Column<T>, sortColumn?: Column<T>;
selectedRow?: T; selectedRow?: T;
onChangePage?: (page: number, pageSize: number) => {}; onChangePage?: (page: number, pageSize: number) => {};
onSort?: (sortColumn: Column<T>) => void; onSort?: (sortColumn: Column<T>) => void;
@ -28,43 +28,85 @@ export interface TableProps<T> extends PublishedTableProps<T> {
secondaryAudit?: boolean; secondaryAudit?: boolean;
} }
interface TableState{ export default function Table<T>(props: TableProps<T>): JSX.Element {
debouncedOnSearch?: any const {
} data,
keyName,
selectedRow,
columns,
sortColumn,
editPath,
canEdit,
canDelete,
onSort,
onChangePage,
onDelete,
onAuditParams,
onSelectRow,
secondaryAudit,
onUnselectRow,
onSearch,
} = props;
class Table<T> extends Component<TableProps<T>, TableState> { const [debouncedOnSearch, setDebouncedOnSearch] = useState<any>(undefined);
state : TableState = {
}
componentDidMount(): void { useEffect(() => {
const {onSearch } = this.props;
const debounceDelay = 200; const debounceDelay = 200;
const debouncedOnSearch = onSearch === undefined ? undefined : debounce(onSearch, debounceDelay); if (!onSearch) {
setDebouncedOnSearch(undefined);
this.setState( { return;
debouncedOnSearch,
})
} }
render() { const d = debounce(onSearch, debounceDelay);
const { data, keyName, selectedRow, columns, sortColumn, editPath, canEdit, canDelete, onSort, onChangePage, onDelete, onAuditParams, onSelectRow, secondaryAudit, onUnselectRow } = this.props; setDebouncedOnSearch(() => d);
const { debouncedOnSearch } = this.state;
const showEdit = (editPath != null) && (editPath !== ""); return () => {
if ((d as any).cancel) (d as any).cancel();
};
}, [onSearch]);
const showEdit = editPath != null && editPath !== "";
const showDelete = onDelete != null; const showDelete = onDelete != null;
const showAudit = onAuditParams != null; const showAudit = onAuditParams != null;
const showSecondaryAudit = showAudit && secondaryAudit === true; const showSecondaryAudit = showAudit && secondaryAudit === true;
return ( return (
<> <>
<table className='table table-sm table-striped table-bordered'> <table className="table table-sm table-striped table-bordered">
<TableHeader columns={columns} sortColumn={sortColumn} showDelete={showDelete} showEdit={showEdit} showAudit={showAudit} showSecondaryAudit={showSecondaryAudit} onSort={onSort} onSearch={debouncedOnSearch} /> <TableHeader
<TableBody data={data.data} keyName={keyName} columns={columns} selectedRow={selectedRow} canEdit={canEdit} canDelete={canDelete} onDelete={onDelete} editPath={editPath} onAuditParams={onAuditParams} onSelectRow={onSelectRow} showSecondaryAudit={showSecondaryAudit}/> columns={columns}
<TableFooter data={data} columns={columns} showDelete={showDelete} showEdit={showEdit} showAudit={showAudit} showSecondaryAudit={showSecondaryAudit} onChangePage={onChangePage} onUnselectRow={onUnselectRow} /> sortColumn={sortColumn}
showDelete={showDelete}
showEdit={showEdit}
showAudit={showAudit}
showSecondaryAudit={showSecondaryAudit}
onSort={onSort}
onSearch={debouncedOnSearch}
/>
<TableBody
data={data.data}
keyName={keyName}
columns={columns}
selectedRow={selectedRow}
canEdit={canEdit}
canDelete={canDelete}
onDelete={onDelete}
editPath={editPath}
onAuditParams={onAuditParams}
onSelectRow={onSelectRow}
showSecondaryAudit={showSecondaryAudit}
/>
<TableFooter
data={data}
columns={columns}
showDelete={showDelete}
showEdit={showEdit}
showAudit={showAudit}
showSecondaryAudit={showSecondaryAudit}
onChangePage={onChangePage}
onUnselectRow={onUnselectRow}
/>
</table> </table>
</> </>
); );
} }
}
export default Table;

View File

@ -1,17 +1,22 @@
import React, { Component } from "react"; import React from "react";
import deepFind from "../../utils/deepfind"; import deepFind from "../../utils/deepfind";
import Column from "./columns"; import Column from "./columns";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBook, faBookJournalWhills, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons"; import {
faBook,
faBookJournalWhills,
faEdit,
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import ConfirmButton from "./ConfirmButton"; import ConfirmButton from "./ConfirmButton";
import { Buffer } from 'buffer'; import { Buffer } from "buffer";
import Button, { ButtonType } from "./Button"; import Button, { ButtonType } from "./Button";
import { DateView } from "./DateView"; import { DateView } from "./DateView";
export interface AuditParams { export interface AuditParams {
entityName : string, entityName: string;
primaryKey : string primaryKey: string;
} }
export interface TableBodyProps<T> { export interface TableBodyProps<T> {
@ -28,111 +33,138 @@ export interface TableBodyProps<T>{
showSecondaryAudit: boolean; showSecondaryAudit: boolean;
} }
class TableBody<T> extends Component<TableBodyProps<T>> { export default function TableBody<T>({
resolvePath = ( path : string, args : string[]) => { data,
keyName,
selectedRow,
columns,
editPath,
canEdit,
canDelete,
onDelete,
onAuditParams,
onSelectRow,
showSecondaryAudit,
}: TableBodyProps<T>): JSX.Element {
const resolvePath = (path: string, args: string[]) => {
let modifiedPath = path; let modifiedPath = path;
let index = 0;
let index : number = 0; while (index < args.length) {
while ( index < args.length ) modifiedPath = modifiedPath.replace("{" + index + "}", args[index]);
{
modifiedPath = modifiedPath.replace("{"+index+"}", args[index])
index++; index++;
} }
return modifiedPath; return modifiedPath;
} };
renderCell = (item : T, column : Column<T>) => { const renderCell = (item: T, column: Column<T>) => {
const {keyName} = this.props;
if (column.content) return column.content(item); if (column.content) return column.content(item);
const foundItem = deepFind(item, column.path || column.key) const foundItem = deepFind(item, column.path || column.key);
let columnContent: JSX.Element; let columnContent: JSX.Element;
if (foundItem instanceof Date) { if (foundItem instanceof Date) {
columnContent = <DateView value={foundItem}/> columnContent = <DateView value={foundItem} />;
} } else if (typeof foundItem === "object") {
else if (typeof foundItem === "object"){
columnContent = <></>; columnContent = <></>;
} } else {
else { columnContent = <>{foundItem}</>;
columnContent = <>
{foundItem}
</>;
} }
const linkPath = column.link; const linkPath = column.link;
if (linkPath !== undefined) { if (linkPath !== undefined) {
const resolvedlinkPath = this.resolvePath( linkPath, [ (item as any)[keyName] ] ); const resolvedlinkPath = resolvePath(linkPath, [(item as any)[keyName]]);
columnContent = <Link to={resolvedlinkPath}>{columnContent}</Link>; columnContent = <Link to={resolvedlinkPath}>{columnContent}</Link>;
} }
return <> return <>{columnContent}</>;
{columnContent}
</>;
}; };
clickRow = ( value : T ) => const clickRow = (value: T) => {
{ if (onSelectRow !== undefined) onSelectRow(value);
const { onSelectRow } = this.props;
if (onSelectRow !== undefined)
onSelectRow( value );
}
createKey = (item : T, column : Column<T>) => {
const { keyName } = this.props;
return (item as any)[keyName] + '_' + (column.path || column.key);
}; };
handleAuditParams = ( item : T, primaryOnly : boolean ) => { const createKey = (item: T, column: Column<T>) => {
const { onAuditParams } = this.props; return (item as any)[keyName] + "_" + (column.path || column.key);
};
const handleAuditParams = (item: T, primaryOnly: boolean) => {
if (onAuditParams !== undefined) { if (onAuditParams !== undefined) {
var auditParams = onAuditParams(item); const auditParams = onAuditParams(item);
let json = JSON.stringify(auditParams); const json = JSON.stringify(auditParams);
var params = Buffer.from(json).toString('base64') ; const params = Buffer.from(json).toString("base64");
var queryString = ""; let queryString = "";
if (primaryOnly===false) if (primaryOnly === false) queryString += "?primaryOnly=" + primaryOnly;
queryString += "?primaryOnly=" + primaryOnly;
return "/audit/" + params + queryString; return "/audit/" + params + queryString;
} }
return ""; return "";
} };
render() {
const { data, keyName, selectedRow, columns, editPath, canEdit, canDelete, onDelete, onAuditParams, showSecondaryAudit } = this.props;
const showDelete: boolean = onDelete != null; const showDelete: boolean = onDelete != null;
const showEdit:boolean = (editPath != null) && (editPath !== ""); const showEdit: boolean = editPath != null && editPath !== "";
const showAudit: boolean = onAuditParams !== undefined; const showAudit: boolean = onAuditParams !== undefined;
return ( return (
<tbody> <tbody>
{data?.map((item) => { {data?.map((item) => {
let classNames = ""; const classNames = selectedRow === item ? "table-primary" : "";
if (selectedRow === item)
{
classNames+="table-primary";
}
return (<tr className={classNames} key={(item as any)[keyName]}> return (
<tr className={classNames} key={(item as any)[keyName]}>
{columns.map((column) => ( {columns.map((column) => (
<td key={this.createKey(item, column)} onClick={ () => this.clickRow(item)}>{this.renderCell(item, column)}</td> <td key={createKey(item, column)} onClick={() => clickRow(item)}>
{renderCell(item, column)}
</td>
))} ))}
{showEdit && <td className="align-middle">{(canEdit === undefined || canEdit(item)) && <Button buttonType={ButtonType.primary} to={this.resolvePath( editPath!, [ (item as any)[keyName] ] )}><FontAwesomeIcon icon={faEdit}/></Button>}</td>} {showEdit && (
{showDelete && <td className="align-middle">{(canDelete === undefined || canDelete(item)) && <ConfirmButton buttonType={ButtonType.primary} keyValue={item} onClick={onDelete} confirmMessage={"Press again to delete"} ><FontAwesomeIcon icon={faTrash}/></ConfirmButton>}</td>} <td className="align-middle">
{showAudit && <td className="align-middle"><Link to={this.handleAuditParams(item, true)}><Button buttonType={ButtonType.primary}><FontAwesomeIcon icon={faBook}/></Button></Link></td>} {(canEdit === undefined || canEdit(item)) && (
{showAudit && showSecondaryAudit && <td className="align-middle"><Link to={this.handleAuditParams(item, false)}><Button buttonType={ButtonType.secondary}><FontAwesomeIcon icon={faBookJournalWhills}/></Button></Link></td>} <Button
</tr>) buttonType={ButtonType.primary}
to={resolvePath(editPath!, [(item as any)[keyName]])}
>
<FontAwesomeIcon icon={faEdit} />
</Button>
)}
</td>
)}
{showDelete && (
<td className="align-middle">
{(canDelete === undefined || canDelete(item)) && (
<ConfirmButton
buttonType={ButtonType.primary}
keyValue={item}
onClick={onDelete}
confirmMessage={"Press again to delete"}
>
<FontAwesomeIcon icon={faTrash} />
</ConfirmButton>
)}
</td>
)}
{showAudit && (
<td className="align-middle">
<Link to={handleAuditParams(item, true)}>
<Button buttonType={ButtonType.primary}>
<FontAwesomeIcon icon={faBook} />
</Button>
</Link>
</td>
)}
{showAudit && showSecondaryAudit && (
<td className="align-middle">
<Link to={handleAuditParams(item, false)}>
<Button buttonType={ButtonType.secondary}>
<FontAwesomeIcon icon={faBookJournalWhills} />
</Button>
</Link>
</td>
)}
</tr>
);
})} })}
</tbody> </tbody>
); );
} }
}
export default TableBody;

View File

@ -1,4 +1,4 @@
import React, { Component } from "react"; import React from "react";
import { Paginated } from "../../services/Paginated"; import { Paginated } from "../../services/Paginated";
import Column from "./columns"; import Column from "./columns";
import Pagination from "./Pagination"; import Pagination from "./Pagination";
@ -14,27 +14,38 @@ export interface TableFooterProps<T>{
onUnselectRow?: () => void; onUnselectRow?: () => void;
} }
class TableFooter<T> extends Component<TableFooterProps<T>> { export default function TableFooter<T>({
render() { data,
const { data, columns, showEdit, showDelete, showAudit, showSecondaryAudit, onChangePage, onUnselectRow} = this.props; columns,
showEdit,
showDelete,
showAudit,
showSecondaryAudit,
onChangePage,
onUnselectRow,
}: TableFooterProps<T>): JSX.Element | null {
let staticColumnCount = 0; let staticColumnCount = 0;
if (showEdit) staticColumnCount++; if (showEdit) staticColumnCount++;
if (showDelete) staticColumnCount++; if (showDelete) staticColumnCount++;
if (showAudit) staticColumnCount++; if (showAudit) staticColumnCount++;
if (showAudit && showSecondaryAudit) staticColumnCount++; if (showAudit && showSecondaryAudit) staticColumnCount++;
let pagination = onChangePage === undefined ? undefined : <Pagination data={data} onChangePage={onChangePage} onUnselect={onUnselectRow} />; const pagination =
onChangePage === undefined ? undefined : (
<Pagination
data={data}
onChangePage={onChangePage}
onUnselect={onUnselectRow}
/>
);
if (pagination) if (!pagination) return null;
return <tfoot>
return (
<tfoot>
<tr> <tr>
<td colSpan={columns.length + staticColumnCount}>{pagination}</td> <td colSpan={columns.length + staticColumnCount}>{pagination}</td>
</tr> </tr>
</tfoot> </tfoot>
);
return <></>
} }
}
export default TableFooter;

View File

@ -1,6 +1,6 @@
import React, { ChangeEvent, useCallback } from "react";
import { faSortAsc, faSortDesc } from "@fortawesome/free-solid-svg-icons"; import { faSortAsc, faSortDesc } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ChangeEvent, Component } from "react";
import Column from "./columns"; import Column from "./columns";
import Input, { InputType } from "./Input"; import Input, { InputType } from "./Input";
@ -15,80 +15,95 @@ export interface TableHeaderProps<T>{
onSearch?: (name: string, value: string) => void; onSearch?: (name: string, value: string) => void;
} }
class TableHeader<T> extends Component<TableHeaderProps<T>> { export default function TableHeader<T>({
columnsMatch = ( left? : Column<T>, right? : Column<T>) => sortColumn,
{ columns,
if (left?.key !== right?.key) return false; showEdit,
return true; showDelete,
showAudit,
showSecondaryAudit,
onSort,
onSearch,
}: TableHeaderProps<T>): JSX.Element {
const columnsMatch = useCallback((left?: Column<T>, right?: Column<T>) => {
return left?.key === right?.key;
}, []);
const raiseSort = useCallback(
(column: Column<T>) => {
let sc = sortColumn;
if (sc) {
if (columnsMatch(column, sc)) {
sc.order = sc.order === "asc" ? "desc" : "asc";
} else {
sc = column;
sc.order = "asc";
} }
raiseSort = (column : Column<T>) => { if (onSort) onSort(sc);
let sortColumn = this.props.sortColumn;
if (sortColumn) {
if (this.columnsMatch(column, sortColumn)){
sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
}
else {
sortColumn = column;
sortColumn.order = "asc";
} }
},
[sortColumn, onSort, columnsMatch],
);
const { onSort } = this.props; const renderSortIcon = useCallback(
(column: Column<T>) => {
if (onSort)
onSort(sortColumn);
}
};
renderSortIcon = (column : Column<T>) => {
const { sortColumn } = this.props;
if (!sortColumn) return null; if (!sortColumn) return null;
if (!columnsMatch(column, sortColumn)) return null;
return sortColumn.order === "asc" ? (
<FontAwesomeIcon icon={faSortAsc} />
) : (
<FontAwesomeIcon icon={faSortDesc} />
);
},
[sortColumn, columnsMatch],
);
if (!this.columnsMatch(column, sortColumn)) return null; const changeSearch = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
if (onSearch) onSearch(e.target.name, e.target.value);
},
[onSearch],
);
if (sortColumn?.order === "asc") return <FontAwesomeIcon icon={faSortAsc}/> const searchRow = onSearch ? (
<tr>
return <FontAwesomeIcon icon={faSortDesc}/> {columns.map((column) => (
};
changeSearch = (e: ChangeEvent<HTMLInputElement>) =>
{
const {onSearch} = this.props;
if (onSearch)
onSearch(e?.target?.name, e?.target?.value);
};
render() {
const { columns, showEdit, showDelete, showAudit, showSecondaryAudit, onSearch } = this.props;
let searchRow = <></>;
if (onSearch)
searchRow = <tr>
{columns.map((column) =>
<th key={column.path || column.key}> <th key={column.path || column.key}>
{ {(column.searchable === undefined || column.searchable === true) && (
(column.searchable === undefined || column.searchable === true ) && <Input
<Input name={column.path || column.key} label={""} error={""} type={InputType.text} onChange={this.changeSearch} /> name={column.path || column.key}
} label={""}
</th> error={""}
type={InputType.text}
onChange={changeSearch}
/>
)} )}
</th>
))}
{showEdit && <th></th>} {showEdit && <th></th>}
{showDelete && <th></th>} {showDelete && <th></th>}
{showAudit && <th></th>} {showAudit && <th></th>}
{showAudit && showSecondaryAudit && <th></th>} {showAudit && showSecondaryAudit && <th></th>}
</tr>; </tr>
) : (
<></>
);
return ( return (
<thead> <thead>
<tr> <tr>
{columns.map((column) => {columns.map((column) => (
<th className="text-nowrap" key={column.path || column.key} scope="col" onClick={() => this.raiseSort(column)}> <th
{column.label} {this.renderSortIcon(column)} className="text-nowrap"
key={column.path || column.key}
scope="col"
onClick={() => raiseSort(column)}
>
{column.label} {renderSortIcon(column)}
</th> </th>
)} ))}
{showEdit && <th scope="col"></th>} {showEdit && <th scope="col"></th>}
{showDelete && <th scope="col"></th>} {showDelete && <th scope="col"></th>}
{showAudit && <th scope="col"></th>} {showAudit && <th scope="col"></th>}
@ -98,6 +113,3 @@ class TableHeader<T> extends Component<TableHeaderProps<T>> {
</thead> </thead>
); );
} }
}
export default TableHeader;

View File

@ -1,8 +1,8 @@
import React from "react"; import React, { useEffect, useState } from "react";
import TextEditor from "./ckeditor/TextEditor"; import TextEditor from "./ckeditor/TextEditor";
import customFieldsService from '../../modules/manager/customfields/services/customFieldsService'; import customFieldsService from "../../modules/manager/customfields/services/customFieldsService";
interface customfieldType { interface CustomFieldType {
type: string; type: string;
name: string; name: string;
guid: string | undefined; guid: string | undefined;
@ -10,57 +10,58 @@ interface customfieldType {
} }
interface TemplateEditorProps { interface TemplateEditorProps {
className : string className: string;
name: string; name: string;
data: string; data: string;
showFields: boolean; showFields: boolean;
onChange: (name: string, value: string) => void; onChange: (name: string, value: string) => void;
} }
interface TemplateEditorState { export default function TemplateEditor({
customfields: Array<customfieldType>; className,
ready : boolean; name,
} data,
showFields,
onChange,
}: TemplateEditorProps) {
const [customfields, setCustomfields] = useState<CustomFieldType[]>([]);
const [ready, setReady] = useState(false);
class TemplateEditor extends React.Component<TemplateEditorProps, TemplateEditorState> { useEffect(() => {
state = { let mounted = true;
customfields : [],
ready: false
}
async componentWillMount() { async function load() {
const pageData = await customFieldsService.getFields(0, 10, "name", true); const pageData = await customFieldsService.getFields(0, 10, "name", true);
const customfields = pageData.data.map( const fields: CustomFieldType[] = pageData.data.map((x: any) => ({
(x) => {
return {
type: "CustomField", type: "CustomField",
name: x.name, name: x.name,
guid: x.guid, guid: x.guid,
id: x.id id: x.id,
}));
if (mounted) {
setCustomfields(fields);
setReady(true);
} }
} }
load();
return () => {
mounted = false;
};
}, []);
if (!ready) return <></>;
return (
<TextEditor
className={className}
name={name}
data={data}
onChange={onChange}
customFields={customfields}
/>
); );
const { className, name, data, onChange } = this.props;
this.setState( {
ready : true,
customfields,
});
} }
render() {
const { className, name, data, onChange } = this.props;
const { ready, customfields } = this.state;
if (!ready)
return <></>
return <TextEditor className={className} name={name} data={data} onChange={onChange} customFields={customfields} />;
}
}
export default TemplateEditor;

View File

@ -1,7 +1,10 @@
import React from "react"; import React from "react";
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef"; import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
import formsService, { CreateFormInstance, EditFormInstance } from "../../modules/manager/forms/services/formsService"; import formsService, {
import parse, { HTMLReactParserOptions, domToReact } from 'html-react-parser'; CreateFormInstance,
EditFormInstance,
} from "../../modules/manager/forms/services/formsService";
import parse, { HTMLReactParserOptions, domToReact } from "html-react-parser";
import { CustomField } from "../../modules/manager/customfields/services/customFieldsService"; import { CustomField } from "../../modules/manager/customfields/services/customFieldsService";
import Form, { FormState } from "./Form"; import Form, { FormState } from "./Form";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@ -14,16 +17,20 @@ interface TemplateFillerProps {
} }
interface TemplateFillerState extends FormState { interface TemplateFillerState extends FormState {
customFields? : CustomField[], customFields?: CustomField[];
template: { template: {
name?: string; name?: string;
templateId? : GeneralIdRef, templateId?: GeneralIdRef;
version? : bigint, version?: bigint;
definition?: string; definition?: string;
} };
} }
class TemplateFiller extends Form<TemplateFillerProps, any, TemplateFillerState> { class TemplateFiller extends Form<
TemplateFillerProps,
any,
TemplateFillerState
> {
state: TemplateFillerState = { state: TemplateFillerState = {
loaded: false, loaded: false,
customFields: undefined, customFields: undefined,
@ -34,30 +41,34 @@ class TemplateFiller extends Form<TemplateFillerProps, any, TemplateFillerState>
definition: undefined, definition: undefined,
}, },
data: {}, data: {},
errors: {} errors: {},
}
schema = {
}; };
schema = {};
componentDidMount(): void { componentDidMount(): void {
this.loadTemplate(); this.loadTemplate();
} }
componentDidUpdate(prevProps: Readonly<TemplateFillerProps>, prevState: Readonly<TemplateFillerState>, snapshot?: any): void { componentDidUpdate(
if ((prevProps.formInstanceId !== this.props.formInstanceId) || (prevProps.templateId !== this.props.templateId)) prevProps: Readonly<TemplateFillerProps>,
prevState: Readonly<TemplateFillerState>,
snapshot?: any,
): void {
if (
prevProps.formInstanceId !== this.props.formInstanceId ||
prevProps.templateId !== this.props.templateId
)
this.loadTemplate(); this.loadTemplate();
if (this.props.onValidationChanged) if (this.props.onValidationChanged) {
{ const prevErrorCount = Object.keys(prevState.errors).length > 0;
const prevErrorCount = Object.keys(prevState.errors).length > 0 const errorCount = Object.keys(this.state.errors).length > 0;
const errorCount = Object.keys(this.state.errors).length > 0
if (prevErrorCount !== errorCount) { if (prevErrorCount !== errorCount) {
this.props.onValidationChanged(); this.props.onValidationChanged();
} }
} }
} }
loadTemplate = async () => { loadTemplate = async () => {
@ -72,7 +83,10 @@ class TemplateFiller extends Form<TemplateFillerProps, any, TemplateFillerState>
loadedData.customFieldValues = undefined; loadedData.customFieldValues = undefined;
loadedData.updatedVersion = undefined; loadedData.updatedVersion = undefined;
} else if (formInstanceId !== undefined) { } else if (formInstanceId !== undefined) {
loadedData = await formsService.getFormInstance(formInstanceId?.id, formInstanceId?.guid); loadedData = await formsService.getFormInstance(
formInstanceId?.id,
formInstanceId?.guid,
);
console.log("formInstanceId", loadedData); console.log("formInstanceId", loadedData);
} else { } else {
loadedData = { loadedData = {
@ -84,8 +98,8 @@ class TemplateFiller extends Form<TemplateFillerProps, any, TemplateFillerState>
customFieldDefinitions: undefined, customFieldDefinitions: undefined,
templateId: undefined, templateId: undefined,
customFieldValues: undefined, customFieldValues: undefined,
updatedVersion : undefined updatedVersion: undefined,
} };
} }
const { template, data } = this.state; const { template, data } = this.state;
@ -99,66 +113,75 @@ class TemplateFiller extends Form<TemplateFillerProps, any, TemplateFillerState>
this.setCustomFieldValues(data, loadedData.customFieldValues, customFields); this.setCustomFieldValues(data, loadedData.customFieldValues, customFields);
this.setState({ loaded: true, template, customFields, data }); this.setState({ loaded: true, template, customFields, data });
} };
parseDefinition = ( definition: string, customFieldDefinitions: CustomField[]) => { parseDefinition = (
definition: string,
customFieldDefinitions: CustomField[],
) => {
const options: HTMLReactParserOptions = { const options: HTMLReactParserOptions = {
replace: (domNode) => { replace: (domNode) => {
const domNodeAsAny: any = domNode; const domNodeAsAny: any = domNode;
if (domNodeAsAny.name === "span") { if (domNodeAsAny.name === "span") {
if (domNodeAsAny.attribs.fieldtype === "CustomField") if (domNodeAsAny.attribs.fieldtype === "CustomField") {
{ const customField = customFieldDefinitions.filter(
const customField = customFieldDefinitions.filter( x => x.guid === domNodeAsAny.attribs.guid)[0]; (x) => x.guid === domNodeAsAny.attribs.guid,
)[0];
return this.renderCustomField(customField, false); return this.renderCustomField(customField, false);
} }
} else if (domNodeAsAny.name === "p") {
return (
<div className="p">
{domToReact(domNodeAsAny.children, options)}
</div>
);
} }
else if (domNodeAsAny.name === "p"){ },
return <div className="p">{domToReact(domNodeAsAny.children, options)}</div> };
}
}
}
return parse(definition, options); return parse(definition, options);
} };
hasValidationErrors = (): boolean => { hasValidationErrors = (): boolean => {
const { errors } = this.state; const { errors } = this.state;
const result = Object.keys(errors).length > 0; const result = Object.keys(errors).length > 0;
return result; return result;
} };
async Save() { async Save() {
const { errors } = this.state; const { errors } = this.state;
const { templateId, version } = this.state.template; const { templateId, version } = this.state.template;
const { formInstanceId } = this.props; const { formInstanceId } = this.props;
if ( Object.keys(errors).length > 0 ) if (Object.keys(errors).length > 0) {
{
toast.error("There are errors on the form"); toast.error("There are errors on the form");
throw new Error("There are errors on the form"); throw new Error("There are errors on the form");
} }
const customFieldValues = this.CustomFieldValues(); const customFieldValues = this.CustomFieldValues();
if (formInstanceId !== undefined) { if (formInstanceId !== undefined) {
if (templateId === undefined) if (templateId === undefined) throw Error("TemplateId cannot be null");
throw Error("TemplateId cannot be null");
if (version === undefined) if (version === undefined) throw Error("Version cannot be null");
throw Error("Version cannot be null");
const editFormInstance : EditFormInstance = { formInstanceId, templateId, version, customFieldValues }; const editFormInstance: EditFormInstance = {
formInstanceId,
templateId,
version,
customFieldValues,
};
await formsService.putFormInstance(editFormInstance); await formsService.putFormInstance(editFormInstance);
} } else {
else { if (templateId !== undefined && version !== undefined) {
if (templateId !== undefined && version !== undefined)
{
//const customFieldValues = this.CustomFieldValues(); //const customFieldValues = this.CustomFieldValues();
const formInstance : CreateFormInstance = { templateId, version, customFieldValues }; const formInstance: CreateFormInstance = {
templateId,
version,
customFieldValues,
};
return await formsService.postFormInstance(formInstance); return await formsService.postFormInstance(formInstance);
} } else throw new Error("template unknown");
else
throw new Error("template unknown");
} }
} }
@ -167,15 +190,15 @@ class TemplateFiller extends Form<TemplateFillerProps, any, TemplateFillerState>
let parsedDefinition: any; let parsedDefinition: any;
if (template.definition) if (template.definition)
parsedDefinition = this.parseDefinition(template.definition, customFields!); parsedDefinition = this.parseDefinition(
else template.definition,
parsedDefinition = <></>; customFields!,
);
else parsedDefinition = <></>;
return ( return (
<Loading loaded={loaded}> <Loading loaded={loaded}>
<div className="ck-content form-editor"> <div className="ck-content form-editor">{parsedDefinition}</div>
{parsedDefinition}
</div>
</Loading> </Loading>
); );
} }

View File

@ -9,10 +9,14 @@ export interface ToggleSliderProps extends ToggleProps {
defaultChecked: boolean; defaultChecked: boolean;
} }
class ToggleSlider extends React.Component<ToggleSliderProps> { export default function ToggleSlider({
render() { name,
const { name, label, error, readOnly, defaultChecked, ...rest } = this.props; label,
error,
readOnly,
defaultChecked,
...rest
}: ToggleSliderProps) {
return ( return (
<div className="form-group"> <div className="form-group">
<label htmlFor={name}>{label}</label> <label htmlFor={name}>{label}</label>
@ -21,6 +25,3 @@ class ToggleSlider extends React.Component<ToggleSliderProps> {
</div> </div>
); );
} }
}
export default ToggleSlider;

View File

@ -1,8 +1,10 @@
import React from "react"; import React, { useEffect, useState, useCallback } from "react";
import Select from "../common/Select"; import Select from "../common/Select";
import Option from "../common/option"; import Option from "../common/option";
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef"; import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
import customFieldsService, { CustomField } from "../../modules/manager/customfields/services/customFieldsService"; import customFieldsService, {
CustomField,
} from "../../modules/manager/customfields/services/customFieldsService";
interface CustomFieldPickerProps { interface CustomFieldPickerProps {
name: string; name: string;
@ -13,82 +15,83 @@ interface CustomFieldPickerProps {
onChange?: (name: string, id: GeneralIdRef, displayValue: string) => void; onChange?: (name: string, id: GeneralIdRef, displayValue: string) => void;
} }
interface CustomFieldPickerState { export default function CustomFieldPicker({
options?: Option[]; name,
} label,
error,
value,
exclude,
onChange,
}: CustomFieldPickerProps) {
const [options, setOptions] = useState<Option[]>([]);
class CustomFieldPicker extends React.Component<CustomFieldPickerProps, CustomFieldPickerState> { useEffect(() => {
state = { options: [] as Option[] }; async function load() {
const pagedData = await customFieldsService.getFields(
async componentDidMount() { 0,
const pagedData = await customFieldsService.getFields(0, 10, "name", true); 10,
"name",
true,
);
if (pagedData) { if (pagedData) {
const options: Option[] = (pagedData.data as any[]).map((x: { id: any; name: any }) => { const opts: Option[] = (pagedData.data as any[]).map(
return { (x: { id: any; name: any }) => ({
_id: x.id, _id: x.id,
name: x.name, name: x.name,
}; }),
}); );
setOptions(opts);
this.setState({ options });
} }
} }
GetOptionById = (value: string ) => { load();
const { options } = this.state; }, []);
for( var option of options) const getOptionById = useCallback(
{ (val: string) => {
if (String(option._id) === value) for (const option of options) {
return option.name; if (String(option._id) === val) return option.name;
} }
return ""; return "";
} },
[options],
);
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleChange = useCallback(
const { onChange } = this.props; (e: React.ChangeEvent<HTMLSelectElement>) => {
const input = e.currentTarget; const input = e.currentTarget;
const generalIdRef = MakeGeneralIdRef(BigInt(input.value)); const generalIdRef = MakeGeneralIdRef(BigInt(input.value));
const displayValue = this.GetOptionById(input.value); const displayValue = getOptionById(input.value);
if (onChange) onChange(input.name, generalIdRef, displayValue); if (onChange) onChange(input.name, generalIdRef, displayValue);
}; },
[onChange, getOptionById],
getFilteredOptions = () => {
const { exclude } = this.props;
const { options } = this.state;
let filteredOptions = options.filter( (o) => {
if (exclude)
{
for( var exlcudedItem of exclude)
{
const idAsString : string = String(exlcudedItem.id);
const oid : string = String(o._id);
if (oid === idAsString) {
return false;
}
}
}
return true;
}
); );
return filteredOptions; const getFilteredOptions = useCallback(() => {
return options.filter((o) => {
if (exclude) {
for (const excludedItem of exclude) {
const idAsString: string = String(excludedItem.id);
const oid: string = String(o._id);
if (oid === idAsString) return false;
} }
}
return true;
});
}, [options, exclude]);
render() { const filteredOptions = getFilteredOptions();
const { name, label, error, value } = this.props;
const filteredOptions = this.getFilteredOptions();
return ( return (
<Select name={name} label={label} error={error} value={value?.id} options={filteredOptions} includeBlankFirstEntry={true} onChange={this.handleChange} /> <Select
name={name}
label={label}
error={error}
value={value?.id}
options={filteredOptions}
includeBlankFirstEntry={true}
onChange={handleChange}
/>
); );
} }
}
export default CustomFieldPicker;

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect, useState, useCallback } from "react";
import Select from "../common/Select"; import Select from "../common/Select";
import Option from "../common/option"; import Option from "../common/option";
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef"; import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
@ -17,101 +17,95 @@ interface DomainPickerProps {
onChange?: (name: string, value: CustomFieldValue[]) => void; onChange?: (name: string, value: CustomFieldValue[]) => void;
} }
interface DomainPickerState { export default function DomainPicker({
options?: Option[]; includeLabel,
selectedOptions: Option[]; name,
} label,
error,
values,
minEntries,
maxEntries,
onChange,
}: DomainPickerProps) {
const [options, setOptions] = useState<Option[]>([]);
const [selectedOptions, setSelectedOptions] = useState<Option[]>([]);
class DomainPicker extends React.Component<DomainPickerProps, DomainPickerState> { useEffect(() => {
state = { options: [] as Option[], selectedOptions: [] as Option[] }; async function load() {
async componentDidMount() {
const { values } = this.props;
const pagedData = await domainsService.getDomains(0, 10, "name", true); const pagedData = await domainsService.getDomains(0, 10, "name", true);
if (pagedData) { if (pagedData) {
const options: Option[] | undefined = pagedData.data.map((x: { id: any; name: any }) => { const opts: Option[] = pagedData.data.map(
return { (x: { id: any; name: any }) => ({
_id: x.id, _id: x.id,
name: x.name, name: x.name,
}; }),
}); );
const selectedOptions: Option[] = []; const selected: Option[] = [];
if (values) { if (values) {
for (const option of values) { for (const option of values) {
const foundOption = options.filter((x) => Number(x._id) === Number((option.value as GeneralIdRef).id))[0]; const foundOption = opts.filter(
selectedOptions.push(foundOption); (x) =>
Number(x._id) === Number((option.value as GeneralIdRef).id),
)[0];
if (foundOption) selected.push(foundOption);
} }
} }
this.setState({ options, selectedOptions }); setOptions(opts);
setSelectedOptions(selected);
} }
} }
doOnChange = (newSelectedOptions: Option[]) => { load();
const { onChange } = this.props; }, [values]);
const { name } = this.props; const doOnChange = useCallback(
(newSelectedOptions: Option[]) => {
var values: CustomFieldValue[] = newSelectedOptions.map((x) => { const vals: CustomFieldValue[] = newSelectedOptions.map((x) => ({
return {
value: MakeGeneralIdRef(x._id as unknown as bigint), value: MakeGeneralIdRef(x._id as unknown as bigint),
displayValue: x.name, displayValue: x.name,
}; }));
});
if (onChange) onChange(name, values); if (onChange) onChange(name, vals);
}; },
[onChange, name],
);
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleChange = useCallback(
let { options, selectedOptions } = this.state; (e: React.ChangeEvent<HTMLSelectElement>) => {
const input = e.currentTarget; const input = e.currentTarget;
const id: number = Number(input.value); const id: number = Number(input.value);
selectedOptions = options.filter((x) => x._id === id); const newSelected = options.filter((x) => x._id === id);
setSelectedOptions(newSelected);
doOnChange(newSelected);
},
[options, doOnChange],
);
this.setState({ selectedOptions }); const handleAdd = useCallback(
this.doOnChange(selectedOptions); (item: Option) => {
}; const newSelected = [...selectedOptions, item];
setSelectedOptions(newSelected);
doOnChange(newSelected);
},
[selectedOptions, doOnChange],
);
handleAdd = (item: Option) => { const handleDelete = useCallback(
const { selectedOptions } = this.state; (item: Option) => {
const newSelected = selectedOptions.filter((x) => x !== item);
setSelectedOptions(newSelected);
doOnChange(newSelected);
},
[selectedOptions, doOnChange],
);
selectedOptions.push(item); if (maxEntries === 1) {
const value = selectedOptions[0]?._id;
this.setState({ selectedOptions });
this.doOnChange(selectedOptions);
};
handleDelete = (item: Option) => {
let { selectedOptions } = this.state;
selectedOptions = selectedOptions.filter((x) => x !== item);
this.setState({ selectedOptions });
this.doOnChange(selectedOptions);
};
render() {
const { includeLabel, name, label, error, values, maxEntries } = this.props;
const { options, selectedOptions } = this.state;
// return (
// <Select
// name={name}
// label={label}
// error={error}
// value={String(value?.id)}
// options={options}
// includeBlankFirstEntry={false}
// onChange={this.handleChange}
// />
// );
if (maxEntries == 1) {
let value = selectedOptions[0]?._id;
return ( return (
<Select <Select
includeLabel={includeLabel} includeLabel={includeLabel}
@ -121,7 +115,7 @@ class DomainPicker extends React.Component<DomainPickerProps, DomainPickerState>
value={value} value={value}
options={options} options={options}
includeBlankFirstEntry={true} includeBlankFirstEntry={true}
onChange={this.handleChange} onChange={handleChange}
/> />
); );
} else { } else {
@ -133,12 +127,9 @@ class DomainPicker extends React.Component<DomainPickerProps, DomainPickerState>
error={error} error={error}
options={options} options={options}
selectedOptions={selectedOptions} selectedOptions={selectedOptions}
onAdd={this.handleAdd} onAdd={handleAdd}
onDelete={this.handleDelete} onDelete={handleDelete}
></MultiSelect> />
); );
} }
} }
}
export default DomainPicker;

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect, useState, useCallback } from "react";
import Select from "../common/Select"; import Select from "../common/Select";
import Option from "../common/option"; import Option from "../common/option";
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef"; import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
@ -13,52 +13,62 @@ interface FormTemplatePickerProps {
onChange?: (name: string, value: GeneralIdRef) => void; onChange?: (name: string, value: GeneralIdRef) => void;
} }
interface FormTemplatePickerState { export default function FormTemplatePicker({
options?: Option[]; includeLabel,
} name,
label,
error,
value,
onChange,
}: FormTemplatePickerProps) {
const [options, setOptions] = useState<Option[]>([]);
class FormTemplatePicker extends React.Component<FormTemplatePickerProps, FormTemplatePickerState> { useEffect(() => {
state = { options: [] as Option[] }; async function load() {
async componentDidMount() {
const formTemplates = await formsService.getForms(0, 10, "name", true); const formTemplates = await formsService.getForms(0, 10, "name", true);
if (formTemplates) { if (formTemplates) {
const options: Option[] | undefined = (formTemplates.data as any[]).map((x) => { const opts: Option[] = (formTemplates.data as any[]).map((x) => ({
return {
_id: x.id, _id: x.id,
name: x.name, name: x.name,
}; }));
}); setOptions(opts);
this.setState({ options });
} }
} }
doOnChange = (name: string, value: bigint) => { load();
const { onChange } = this.props; }, []);
const generalIdRef = MakeGeneralIdRef(value); const doOnChange = useCallback(
if (onChange) onChange(name, generalIdRef); (n: string, v: bigint) => {
}; const generalIdRef = MakeGeneralIdRef(v);
if (onChange) onChange(n, generalIdRef);
},
[onChange],
);
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const input = e.currentTarget; const input = e.currentTarget;
this.doOnChange(input.name, BigInt(input.value)); doOnChange(input.name, BigInt(input.value));
}; },
[doOnChange],
render() { );
const { includeLabel, name, label, error, value } = this.props;
const { options } = this.state;
let id = ""; let id = "";
if (!((value === undefined || Number.isNaN(value.id) ))) { if (value !== undefined && !Number.isNaN(value.id)) {
id = String(value.id); id = String(value.id);
} }
return ( return (
<Select includeLabel={includeLabel} name={name} label={label} error={error} value={id} options={options} includeBlankFirstEntry={true} onChange={this.handleChange} /> <Select
includeLabel={includeLabel}
name={name}
label={label}
error={error}
value={id}
options={options}
includeBlankFirstEntry={true}
onChange={handleChange}
/>
); );
} }
}
export default FormTemplatePicker;

View File

@ -1,8 +1,11 @@
import React from "react"; import React, { useEffect, useState, useCallback } from "react";
import Select from "../common/Select"; import Select from "../common/Select";
import Option from "../common/option"; import Option from "../common/option";
import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef"; import { GeneralIdRef, MakeGeneralIdRef } from "../../utils/GeneralIdRef";
import glossariesService, { CustomFieldValue, SystemGlossaries } from "../../modules/manager/glossary/services/glossaryService"; import glossariesService, {
CustomFieldValue,
SystemGlossaries,
} from "../../modules/manager/glossary/services/glossaryService";
import MultiSelect from "../common/MultiSelect"; import MultiSelect from "../common/MultiSelect";
interface GlossaryPickerProps { interface GlossaryPickerProps {
@ -16,94 +19,96 @@ interface GlossaryPickerProps {
onChange?: (name: string, values: CustomFieldValue[]) => void; onChange?: (name: string, values: CustomFieldValue[]) => void;
} }
interface GlossaryPickerState { export default function GlossaryPicker({
options?: Option[]; includeLabel,
selectedOptions: Option[]; name,
} label,
rootItem,
error,
values,
maxEntries,
onChange,
}: GlossaryPickerProps) {
const [options, setOptions] = useState<Option[]>([]);
const [selectedOptions, setSelectedOptions] = useState<Option[]>([]);
class GlossaryPicker extends React.Component<GlossaryPickerProps, GlossaryPickerState> { useEffect(() => {
state = { async function load() {
options: [] as Option[],
selectedOptions: [] as Option[],
};
async componentDidMount() {
const { rootItem, values } = this.props;
const actualRootItem = rootItem ?? SystemGlossaries; const actualRootItem = rootItem ?? SystemGlossaries;
const glossary = await glossariesService.getGlossaryItem(actualRootItem); const glossary = await glossariesService.getGlossaryItem(actualRootItem);
if (glossary) { if (glossary) {
const options: Option[] | undefined = glossary.children.map((x: { id: any; name: any }) => { const opts: Option[] = glossary.children.map(
return { (x: { id: any; name: any }) => ({
_id: x.id, _id: x.id,
name: x.name, name: x.name,
}; }),
}); );
const selectedOptions: Option[] = []; const selected: Option[] = [];
if (values) { if (values) {
for (const option of values) { for (const option of values) {
const foundOption = options.filter((x) => Number(x._id) === Number((option.value as GeneralIdRef).id))[0]; const foundOption = opts.filter(
selectedOptions.push(foundOption); (x) =>
Number(x._id) === Number((option.value as GeneralIdRef).id),
)[0];
if (foundOption) selected.push(foundOption);
} }
} }
this.setState({ options, selectedOptions }); setOptions(opts);
setSelectedOptions(selected);
} }
} }
doOnChange = (newSelectedOptions: Option[]) => { load();
const { onChange } = this.props; }, [rootItem, values]);
const { name } = this.props; const doOnChange = useCallback(
(newSelectedOptions: Option[]) => {
var values: CustomFieldValue[] = newSelectedOptions.map((x) => { const vals: CustomFieldValue[] = newSelectedOptions.map((x) => ({
return {
value: MakeGeneralIdRef(x._id as unknown as bigint), value: MakeGeneralIdRef(x._id as unknown as bigint),
displayValue: x.name, displayValue: x.name,
}; }));
});
if (onChange) onChange(name, values); if (onChange) onChange(name, vals);
}; },
[onChange, name],
);
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleChange = useCallback(
let { options, selectedOptions } = this.state; (e: React.ChangeEvent<HTMLSelectElement>) => {
const input = e.currentTarget; const input = e.currentTarget;
const id: number = Number(input.value); const id: number = Number(input.value);
selectedOptions = options.filter((x) => x._id === id); const newSelected = options.filter((x) => x._id === id);
setSelectedOptions(newSelected);
doOnChange(newSelected);
},
[options, doOnChange],
);
this.setState({ selectedOptions }); const handleAdd = useCallback(
this.doOnChange(selectedOptions); (item: Option) => {
}; const newSelected = [...selectedOptions, item];
setSelectedOptions(newSelected);
doOnChange(newSelected);
},
[selectedOptions, doOnChange],
);
handleAdd = (item: Option) => { const handleDelete = useCallback(
const { selectedOptions } = this.state; (item: Option) => {
const newSelected = selectedOptions.filter((x) => x !== item);
setSelectedOptions(newSelected);
doOnChange(newSelected);
},
[selectedOptions, doOnChange],
);
selectedOptions.push(item); if (maxEntries === 1) {
const value = selectedOptions[0]?._id;
this.setState({ selectedOptions });
this.doOnChange(selectedOptions);
};
handleDelete = (item: Option) => {
let { selectedOptions } = this.state;
selectedOptions = selectedOptions.filter((x) => x !== item);
this.setState({ selectedOptions });
this.doOnChange(selectedOptions);
};
render() {
const { includeLabel, name, label, error, maxEntries } = this.props;
const { options, selectedOptions } = this.state;
if (maxEntries == 1) {
let value = selectedOptions[0]?._id;
return ( return (
<Select <Select
includeLabel={includeLabel} includeLabel={includeLabel}
@ -113,7 +118,7 @@ class GlossaryPicker extends React.Component<GlossaryPickerProps, GlossaryPicker
value={value} value={value}
options={options} options={options}
includeBlankFirstEntry={true} includeBlankFirstEntry={true}
onChange={this.handleChange} onChange={handleChange}
/> />
); );
} else { } else {
@ -125,12 +130,9 @@ class GlossaryPicker extends React.Component<GlossaryPickerProps, GlossaryPicker
error={error} error={error}
options={options} options={options}
selectedOptions={selectedOptions} selectedOptions={selectedOptions}
onAdd={this.handleAdd} onAdd={handleAdd}
onDelete={this.handleDelete} onDelete={handleDelete}
></MultiSelect> />
); );
} }
} }
}
export default GlossaryPicker;

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect, useState, useCallback } from "react";
import sequenceService from "../../modules/manager/sequence/services/sequenceService"; import sequenceService from "../../modules/manager/sequence/services/sequenceService";
import Select from "./../common/Select"; import Select from "./../common/Select";
import Option from "../common/option"; import Option from "../common/option";
@ -13,46 +13,55 @@ interface SequencePickerProps {
onChange?: (name: string, value: GeneralIdRef) => void; onChange?: (name: string, value: GeneralIdRef) => void;
} }
interface SequencePickerState { export default function SequencePicker({
options?: Option[]; includeLabel,
} name,
label,
error,
value,
onChange,
}: SequencePickerProps) {
const [options, setOptions] = useState<Option[] | undefined>(undefined);
class SequencePicker extends React.Component<SequencePickerProps, SequencePickerState> { useEffect(() => {
state = { options: undefined }; async function load() {
async componentDidMount() {
const pagedData = await sequenceService.getSequences(0, 10, "name", true); const pagedData = await sequenceService.getSequences(0, 10, "name", true);
if (pagedData) { if (pagedData) {
const options: Option[] = (pagedData.data as any[]).map((x: { id: any; name: any }) => { const opts: Option[] = (pagedData.data as any[]).map(
return { (x: { id: any; name: any }) => ({
_id: x.id, _id: x.id,
name: x.name, name: x.name,
}; }),
}); );
setOptions(opts);
this.setState({ options });
} }
} }
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { load();
const { onChange } = this.props; }, []);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const input = e.currentTarget; const input = e.currentTarget;
const generalIdRef: GeneralIdRef = { const generalIdRef: GeneralIdRef = {
id: BigInt(input.value), id: BigInt(input.value),
}; };
if (onChange) onChange(input.name, generalIdRef); if (onChange) onChange(input.name, generalIdRef);
}; },
[onChange],
render() { );
const { includeLabel, name, label, error, value } = this.props;
const { options } = this.state;
return ( return (
<Select includeLabel={includeLabel} name={name} label={label} error={error} value={String(value?.id)} options={options} includeBlankFirstEntry={true} onChange={this.handleChange} /> <Select
includeLabel={includeLabel}
name={name}
label={label}
error={error}
value={String(value?.id)}
options={options}
includeBlankFirstEntry={true}
onChange={handleChange}
/>
); );
} }
}
export default SequencePicker;

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect, useState, useCallback } from "react";
import Select from "../common/Select"; import Select from "../common/Select";
import Option from "../common/option"; import Option from "../common/option";
import { GeneralIdRef } from "../../utils/GeneralIdRef"; import { GeneralIdRef } from "../../utils/GeneralIdRef";
@ -13,46 +13,57 @@ interface SsoProviderPickerProps {
onChange?: (name: string, value: GeneralIdRef) => void; onChange?: (name: string, value: GeneralIdRef) => void;
} }
interface SsoProviderPickerState { export default function SsoProviderPicker({
options?: Option[]; name,
} label,
error,
value,
domain,
onChange,
}: SsoProviderPickerProps) {
const [options, setOptions] = useState<Option[] | undefined>(undefined);
class SsoProviderPicker extends React.Component<SsoProviderPickerProps, SsoProviderPickerState> { useEffect(() => {
state = { options: undefined }; async function load() {
const pagedData = await ssoManagerService.getSsoProviders(
async componentDidMount() { 0,
const pagedData = await ssoManagerService.getSsoProviders(0, 10, "name", true); 10,
"name",
true,
);
if (pagedData) { if (pagedData) {
const options: Option[] = (pagedData.data as any[]).map(x => { const opts: Option[] = (pagedData.data as any[]).map((x) => ({
return {
_id: x.id, _id: x.id,
name: x.name, name: x.name,
}; }));
}); setOptions(opts);
this.setState({ options });
} }
} }
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { load();
const { onChange } = this.props; }, []);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const input = e.currentTarget; const input = e.currentTarget;
const generalIdRef: GeneralIdRef = { const generalIdRef: GeneralIdRef = {
id: BigInt(input.value), id: BigInt(input.value),
}; };
if (onChange) onChange(input.name, generalIdRef); if (onChange) onChange(input.name, generalIdRef);
}; },
[onChange],
render() { );
const { name, label, error, value } = this.props;
const { options } = this.state;
return ( return (
<Select name={name} label={label} error={error} value={String(value?.id)} options={options} includeBlankFirstEntry={true} onChange={this.handleChange} /> <Select
name={name}
label={label}
error={error}
value={String(value?.id)}
options={options}
includeBlankFirstEntry={true}
onChange={handleChange}
/>
); );
} }
}
export default SsoProviderPicker;

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect, useState, useCallback } from "react";
import Select from "./../common/Select"; import Select from "./../common/Select";
import Option from "../common/option"; import Option from "../common/option";
import { GeneralIdRef } from "./../../utils/GeneralIdRef"; import { GeneralIdRef } from "./../../utils/GeneralIdRef";
@ -13,46 +13,52 @@ interface UserPickerProps {
onChange?: (name: string, value: GeneralIdRef) => void; onChange?: (name: string, value: GeneralIdRef) => void;
} }
interface UserPickerState { export default function UserPicker({
options?: Option[]; name,
} label,
error,
value,
domain,
onChange,
}: UserPickerProps) {
const [options, setOptions] = useState<Option[] | undefined>(undefined);
class UserPicker extends React.Component<UserPickerProps, UserPickerState> { useEffect(() => {
state = { options: undefined }; async function load() {
async componentDidMount() {
const pagedData = await userService.getUsers(0, 10, "name", true); const pagedData = await userService.getUsers(0, 10, "name", true);
if (pagedData) { if (pagedData) {
const options: Option[] = (pagedData.data as any[]).map(x => { const opts: Option[] = (pagedData.data as any[]).map((x) => ({
return {
_id: x.id, _id: x.id,
name: x.displayName, name: x.displayName,
}; }));
}); setOptions(opts);
this.setState({ options });
} }
} }
handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => { load();
const { onChange } = this.props; }, []);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const input = e.currentTarget; const input = e.currentTarget;
const generalIdRef: GeneralIdRef = { const generalIdRef: GeneralIdRef = {
id: BigInt(input.value), id: BigInt(input.value),
}; };
if (onChange) onChange(input.name, generalIdRef); if (onChange) onChange(input.name, generalIdRef);
}; },
[onChange],
render() { );
const { name, label, error, value } = this.props;
const { options } = this.state;
return ( return (
<Select name={name} label={label} error={error} value={String(value?.id)} options={options} includeBlankFirstEntry={true} onChange={this.handleChange} /> <Select
name={name}
label={label}
error={error}
value={String(value?.id)}
options={options}
includeBlankFirstEntry={true}
onChange={handleChange}
/>
); );
} }
}
export default UserPicker;

View File

@ -1,114 +1,118 @@
import React, { Component } from 'react'; import React, { useEffect, useState, useCallback } from "react";
import Column from '../../components/common/columns'; import Column from "../../components/common/columns";
import { Paginated } from '../../services/Paginated'; import { Paginated } from "../../services/Paginated";
import withRouter from '../../utils/withRouter'; import AuditTable from "./components/auditTable";
import AuditTable from './components/auditTable'; import auditService, { AuditLogEntry } from "./services/auditService";
import auditService, { AuditLogEntry } from './services/auditService'; import Loading from "../../components/common/Loading";
import equal from "fast-deep-equal"; import { useLocation, useParams } from "react-router-dom";
import Loading from '../../components/common/Loading';
interface AuditState{ const useQuery = () => new URLSearchParams(useLocation().search);
loaded: boolean;
pagedData : Paginated<AuditLogEntry>,
sortColumn : Column<AuditLogEntry>,
filters: Map<string, string>;
}
class Audit extends Component< any, any, AuditState> { export default function Audit() {
state = { const location = useLocation();
loaded: false, const params = useParams();
pagedData : { page: 1, const query = useQuery();
const [loaded, setLoaded] = useState(false);
const [pagedData, setPagedData] = useState<Paginated<AuditLogEntry>>({
page: 1,
pageSize: 10, pageSize: 10,
count: 0, count: 0,
totalPages: 1, totalPages: 1,
data: [] }, data: [],
sortColumn: { key: "dateTime", label: "Timing", order: "desc" }, });
filters: new Map<string, string>()
}
componentDidMount = () => { const [sortColumn, setSortColumn] = useState<Column<AuditLogEntry>>({
this.loadData(); key: "dateTime",
label: "Timing",
order: "desc",
});
const [filters, setFilters] = useState<Map<string, string>>(new Map());
const doSearch = useCallback(async () => {
const { page, pageSize } = pagedData;
const auditId = params.auditId ?? "";
const primaryOnly = query.get("primaryOnly");
const isPrimaryOnly =
primaryOnly === null || primaryOnly.toLowerCase() === "true";
const result = await auditService.getLog(
auditId,
isPrimaryOnly,
page,
pageSize,
sortColumn.key,
sortColumn.order === "asc",
filters,
);
if (result) {
setPagedData(result);
setLoaded(true);
} else {
setLoaded(false);
}
}, [pagedData, sortColumn, filters, params.auditId, query]);
const changePage = async (page: number, pageSize: number) => {
setPagedData((prev) => ({
...prev,
page,
pageSize,
}));
setLoaded(false);
}; };
componentDidUpdate(prevProps: Readonly<any>, prevState: Readonly<any>, snapshot?: AuditState | undefined): void { const onSort = async (column: Column<AuditLogEntry>) => {
if (!(equal(this.props?.router.location.search, prevProps?.router.location.search) && equal(this.props?.router.params.auditId, prevProps?.router.params.auditId))) setSortColumn(column);
{ setLoaded(false);
let {pagedData, sortColumn} = this.state; };
pagedData = { page: 1, const onSearch = async (name: string, value: string) => {
setFilters((prev) => {
const updated = new Map(prev);
updated.set(name, value);
return updated;
});
setLoaded(false);
};
// Load data on mount and whenever page/sort/filter changes
useEffect(() => {
doSearch();
}, [doSearch]);
// Reset pagination + sort when URL search or auditId changes
useEffect(() => {
setPagedData({
page: 1,
pageSize: 10, pageSize: 10,
count: 0, count: 0,
totalPages: 1, totalPages: 1,
data: [] }; data: [],
sortColumn = { key: "dateTime", label: "Timing", order: "desc" }; });
this.setState({pagedData, sortColumn}); setSortColumn({
key: "dateTime",
label: "Timing",
order: "desc",
});
this.changePage(pagedData.page, pagedData.pageSize); setLoaded(false);
} }, [location.search, params.auditId]);
}
loadData = async () => {
const { page, pageSize } = this.state.pagedData;
await this.changePage(page, pageSize);
}
changePage = async(page: number, pageSize : number) =>{
const { pagedData } = this.state;
pagedData.page = page;
pagedData.pageSize = pageSize;
this.setState( {loaded: false, pagedData } );
await this.doSearch();
}
onSort = async(sortColumn : Column<AuditLogEntry>) => {
this.setState({loaded: false, sortColumn});
await this.doSearch();
}
onSearch = async ( name: string, value: string) => {
const { filters } = this.state;
filters.set(name, value);
this.setState( { filters });
await this.doSearch();
};
doSearch = async () => {
const {page, pageSize } = this.state.pagedData;
const {sortColumn, filters } = this.state;
const auditId = this.props?.router?.params?.auditId ?? "";
var primaryOnly = this.props.router.query.get("primaryOnly");
const isPrimaryOnly : boolean = (primaryOnly === null) || (primaryOnly.toLowerCase() === "true");
const pagedData = await auditService.getLog(auditId, isPrimaryOnly, page, pageSize, sortColumn.key, sortColumn.order === "asc", filters);
if (pagedData) {
this.setState({ loaded: true, pagedData });
}
else {
this.setState({ loaded: false });
}
}
render(): JSX.Element {
const {loaded, pagedData, sortColumn } = this.state;
return ( return (
<Loading loaded={loaded}> <Loading loaded={loaded}>
<div> <div>
<AuditTable data={pagedData} sortColumn={sortColumn} onChangePage={this.changePage} onSort={this.onSort} onSearch={this.onSearch}/> <AuditTable
data={pagedData}
sortColumn={sortColumn}
onChangePage={changePage}
onSort={onSort}
onSearch={onSearch}
/>
</div> </div>
</Loading> </Loading>
); );
} }
};
const HOCAudit = withRouter(Audit);
export default HOCAudit;

View File

@ -1,14 +1,16 @@
import React from "react"; import React, { useCallback, useMemo } from "react";
import Column from "../../../components/common/columns"; import Column from "../../../components/common/columns";
import Table, { PublishedTableProps } from "../../../components/common/Table"; import Table, { PublishedTableProps } from "../../../components/common/Table";
import { Paginated } from "../../../services/Paginated"; import { Paginated } from "../../../services/Paginated";
import { AuditLogEntry } from "../services/auditService"; import { AuditLogEntry } from "../services/auditService";
import { Namespaces } from "../../../i18n/i18n";
import { useTranslation } from "react-i18next";
interface AuditFieldValues { interface AuditFieldValues {
OldDisplayName? : string, OldDisplayName?: string;
OldValue? : string, OldValue?: string;
NewDisplayName? : string NewDisplayName?: string;
NewValue? : string NewValue?: string;
} }
interface AuditFieldChanges { interface AuditFieldChanges {
@ -17,103 +19,143 @@ interface AuditFieldChanges{
interface AuditFieldChangeValues { interface AuditFieldChangeValues {
fieldName: string; fieldName: string;
oldDisplayName? : string, oldDisplayName?: string;
oldValue? : string, oldValue?: string;
newDisplayName? : string newDisplayName?: string;
newValue? : string newValue?: string;
} }
class AuditTable extends React.Component<PublishedTableProps<AuditLogEntry>> { export default function AuditTable(props: PublishedTableProps<AuditLogEntry>) {
fieldColumns : Column<AuditFieldChangeValues>[] = [ const { data, sortColumn, onChangePage, onSearch, onSort } = props;
{ key: "fieldName", label: "Field" }, const { t } = useTranslation<typeof Namespaces.Common>();
{ key: "oldDisplayName", label: "Old Value", content: (item) => {
if (item.oldDisplayName !== undefined)
return <>{item.oldDisplayName}</>
return <>{item.oldValue !== undefined ? String(item.oldValue) : ""}</>
} },
{ key: "newDisplayName", label: "New Value", content: (item) => {
if (item.newDisplayName !== undefined)
return <>{item.newDisplayName}</>
return <>{item.newValue !== undefined ? String(item.newValue) : ""}</>
} },
]
columns : Column<AuditLogEntry>[] = [ const fieldColumns: Column<AuditFieldChangeValues>[] = useMemo(
{ key: "dateTime", label: "Timing", order: "asc", searchable: false }, () => [
{ key: "userDisplayName", label: "User Name", order: "asc" }, { key: "fieldName", label: t("Field") },
{ key: "comment", label: "Comment", order: "asc" },
{ key: "entityDisplayName", label: "Entity Display Name", order: "asc", searchable: false },
{ key: "type", label: "Type", order: "asc" },
{ key: "displayName", label: "DisplayName", order: "asc" },
{ key: "fields", label: "Changes", order: "asc", searchable: false, content: (item)=>{
if (item.type === "Delete" || item.type === "Purge")
{ {
if (item.displayName !== "") key: "oldDisplayName",
return <></> label: t("OldValue"),
content: (item) => {
if (item.oldDisplayName !== undefined)
return <>{item.oldDisplayName}</>;
return (
<>{item.oldValue !== undefined ? String(item.oldValue) : ""}</>
);
},
},
{
key: "newDisplayName",
label: t("NewValue"),
content: (item) => {
if (item.newDisplayName !== undefined)
return <>{item.newDisplayName}</>;
return (
<>{item.newValue !== undefined ? String(item.newValue) : ""}</>
);
},
},
],
[t],
);
const columns: Column<AuditLogEntry>[] = useMemo(
() => [
{ key: "dateTime", label: t("Timing"), order: "asc", searchable: false },
{ key: "userDisplayName", label: t("User Name"), order: "asc" },
{ key: "comment", label: t("Comment"), order: "asc" },
{
key: "entityDisplayName",
label: t("EntityDisplayName"),
order: "asc",
searchable: false,
},
{ key: "type", label: t("Type"), order: "asc" },
{ key: "displayName", label: t("DisplayName"), order: "asc" },
{
key: "fields",
label: t("Changes"),
order: "asc",
searchable: false,
content: (item) => {
if (
(item.type === "Delete" || item.type === "Purge") &&
item.displayName !== ""
) {
return <></>;
} }
const fieldsObject: AuditFieldChanges = JSON.parse(item!.fields); const fieldsObject: AuditFieldChanges = JSON.parse(item!.fields);
const data: AuditFieldChangeValues[] = [];
let data : AuditFieldChangeValues[] = []; Object.keys(fieldsObject).forEach((key) => {
Object.keys(fieldsObject).forEach( const dataItem: AuditFieldChangeValues = {
(key, index) => {
let dataItem : AuditFieldChangeValues =
{
fieldName: key, fieldName: key,
oldValue: fieldsObject[key].OldValue, oldValue: fieldsObject[key].OldValue,
oldDisplayName: fieldsObject[key].OldDisplayName, oldDisplayName: fieldsObject[key].OldDisplayName,
newValue: fieldsObject[key].NewValue, newValue: fieldsObject[key].NewValue,
newDisplayName : fieldsObject[key].NewDisplayName newDisplayName: fieldsObject[key].NewDisplayName,
} };
data.push(dataItem); data.push(dataItem);
}); });
let paginated : Paginated<AuditFieldChangeValues> = { const paginated: Paginated<AuditFieldChangeValues> = {
count: 0, count: 0,
page: 1, page: 1,
pageSize: 10, pageSize: 10,
totalPages: 0, totalPages: 0,
data: data data,
} };
let fieldColumns : Column<AuditFieldChangeValues>[]; let displayColumns: Column<AuditFieldChangeValues>[] = [
fieldColumns = this.fieldColumns; ...fieldColumns,
if (item.type === "Create" || item.type === "Restore")
{
fieldColumns = this.fieldColumns.filter( x => x.key !== "oldDisplayName");
}
if (item.type === "Delete" || item.type === "Purge")
{
fieldColumns = this.fieldColumns.filter( x => x.key !== "newDisplayName");
}
return <Table data={ paginated } keyName="id" columns={fieldColumns} />;
} },
]; ];
raiseSort = (sortColumn : Column<AuditLogEntry>) => { if (item.type === "Create" || item.type === "Restore") {
this.setState({sortColumn}); displayColumns = displayColumns.filter(
if (this.props.onSort !== undefined) (x) => x.key !== "oldDisplayName",
this.props.onSort(sortColumn); );
} }
handleAuditParams = (item: any) => { if (item.type === "Delete" || item.type === "Purge") {
displayColumns = displayColumns.filter(
(x) => x.key !== "newDisplayName",
);
}
return (
<Table data={paginated} keyName="id" columns={displayColumns} />
);
},
},
],
[fieldColumns, t],
);
const raiseSort = useCallback(
(sortCol: Column<AuditLogEntry>) => {
if (onSort !== undefined) onSort(sortCol);
},
[onSort],
);
const handleAuditParams = useCallback((item: any) => {
return { return {
entityName: item.entityName, entityName: item.entityName,
primaryKey : item.primaryKey primaryKey: item.primaryKey,
} };
} }, []);
render() { return (
const { data, sortColumn, onChangePage, onSearch } = this.props; <Table
data={data}
return <Table data={ data } keyName="id" columns={this.columns} sortColumn={sortColumn} onSort={this.raiseSort} onChangePage={onChangePage} onSearch={onSearch} onAuditParams={this.handleAuditParams} secondaryAudit={true} />; keyName="id"
columns={columns}
sortColumn={sortColumn}
onSort={raiseSort}
onChangePage={onChangePage}
onSearch={onSearch}
onAuditParams={handleAuditParams}
secondaryAudit={true}
/>
);
} }
}
export default AuditTable;

View File

@ -1,81 +1,110 @@
import React, { Component } from 'react'; import React, { useCallback, useEffect, useState } from "react";
import { toast } from 'react-toastify'; import { toast } from "react-toastify";
import Column from '../../components/common/columns'; import Column from "../../components/common/columns";
import { Paginated } from '../../services/Paginated'; import { Paginated } from "../../services/Paginated";
import BlockedIPsTable from './components/blockedIPsTable'; import BlockedIPsTable from "./components/blockedIPsTable";
import blockedIPsService, { BlockedIPEntry } from './services/blockedIPsService'; import blockedIPsService, {
BlockedIPEntry,
} from "./services/blockedIPsService";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../i18n/i18n";
interface BlockedIPsState { export default function BlockedIPs() {
pagedData: Paginated<BlockedIPEntry>, const { t } = useTranslation<typeof Namespaces.Common>();
sortColumn: Column<BlockedIPEntry>, const [pagedData, setPagedData] = useState<Paginated<BlockedIPEntry>>({
filters: Map<string, string>;
}
class BlockedIPs extends Component<any, any, BlockedIPsState> {
state = {
pagedData: {
page: 1, page: 1,
pageSize: 10, pageSize: 10,
count: 0, count: 0,
totalPages: 1, totalPages: 1,
data: [] data: [],
});
const [sortColumn, setSortColumn] = useState<Column<BlockedIPEntry>>({
key: "ipAddress",
label: t("IPAddress"),
order: "asc",
});
const [filters, setFilters] = useState<Map<string, string>>(
() => new Map<string, string>(),
);
const loadPage = useCallback(
async (page: number, pageSize: number) => {
const paged = await blockedIPsService.getBlockedIps(
page,
pageSize,
sortColumn.key,
sortColumn.order === "asc",
filters,
);
if (paged) setPagedData(paged);
}, },
sortColumn: { key: "ipAddress", label: "IP Address", order: "asc" }, [sortColumn, filters],
filters: new Map<string, string>() );
}
componentDidMount = async () => { useEffect(() => {
const { page, pageSize } = this.state.pagedData; loadPage(pagedData.page, pagedData.pageSize);
}, []); // initial load only
await this.changePage(page, pageSize); const handleSort = useCallback(
} async (col: Column<BlockedIPEntry>) => {
setSortColumn(col);
changePage = async (page: number, pageSize: number) => { const paged = await blockedIPsService.getBlockedIps(
const { sortColumn, filters } = this.state; pagedData.page,
pagedData.pageSize,
col.key,
col.order === "asc",
filters,
);
const pagedData = await blockedIPsService.getBlockedIps(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); if (paged) setPagedData(paged);
if (pagedData) { },
this.setState({ pagedData }); [pagedData.page, pagedData.pageSize, filters],
} );
}
onSort = async (sortColumn: Column<BlockedIPEntry>) => { const handleSearch = useCallback(
const { page, pageSize } = this.state.pagedData; async (name: string, value: string) => {
const { filters } = this.state; const newFilters = new Map(filters);
const pagedData = await blockedIPsService.getBlockedIps(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); newFilters.set(name, value);
if (pagedData) { setFilters(newFilters);
this.setState({ pagedData, sortColumn });
}
}
onSearch = async (name: string, value: string) => { const paged = await blockedIPsService.getBlockedIps(
const { page, pageSize } = this.state.pagedData; pagedData.page,
const { sortColumn, filters } = this.state; pagedData.pageSize,
filters.set(name, value); sortColumn.key,
sortColumn.order === "asc",
newFilters,
);
const pagedData = await blockedIPsService.getBlockedIps(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); if (paged) setPagedData(paged);
if (pagedData) { },
this.setState({ filters, pagedData }); [filters, pagedData.page, pagedData.pageSize, sortColumn],
} );
};
onUnblock = async (item?: BlockedIPEntry) => { const handleUnblock = useCallback(
async (item?: BlockedIPEntry) => {
const response = await blockedIPsService.UnBlockIp(item?.ipAddress); const response = await blockedIPsService.UnBlockIp(item?.ipAddress);
if (response) { if (response) {
this.componentDidMount(); await loadPage(pagedData.page, pagedData.pageSize);
toast.info(`IP Address '${item?.ipAddress}' Unblocked.`); toast.info(t("IPAddressUnblocked", { ip: item?.ipAddress }));
} }
} },
[pagedData.page, pagedData.pageSize, loadPage, t],
render(): JSX.Element { );
const { pagedData, sortColumn } = this.state;
return ( return (
<div> <div>
<BlockedIPsTable data={pagedData} sortColumn={sortColumn} onChangePage={this.changePage} onSort={this.onSort} onSearch={this.onSearch} onDelete={this.onUnblock} /> <BlockedIPsTable
data={pagedData}
sortColumn={sortColumn}
onChangePage={loadPage}
onSort={handleSort}
onSearch={handleSearch}
onDelete={handleUnblock}
/>
</div> </div>
); );
} }
};
export default BlockedIPs;

View File

@ -1,39 +1,78 @@
import { faUnlock } from "@fortawesome/free-solid-svg-icons"; import { faUnlock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React from "react"; import React, { useCallback, useMemo } from "react";
import { ButtonType } from "../../../components/common/Button"; import { ButtonType } from "../../../components/common/Button";
import Column from "../../../components/common/columns"; import Column from "../../../components/common/columns";
import ConfirmButton from "../../../components/common/ConfirmButton"; import ConfirmButton from "../../../components/common/ConfirmButton";
import Table, { PublishedTableProps } from "../../../components/common/Table"; import Table, { PublishedTableProps } from "../../../components/common/Table";
import authentication from "../../frame/services/authenticationService"; import authentication from "../../frame/services/authenticationService";
import { BlockedIPEntry } from "../services/blockedIPsService"; import { BlockedIPEntry } from "../services/blockedIPsService";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../../i18n/i18n";
class BlockedIPsTable extends React.Component<PublishedTableProps<BlockedIPEntry>> { export default function BlockedIPsTable(
canUnblockBlockedIPAddress = authentication.hasAccess("UnlockIPAddress"); props: PublishedTableProps<BlockedIPEntry>,
) {
const { t } = useTranslation<typeof Namespaces.Common>();
const { data, sortColumn, onChangePage, onSearch, onSort, onDelete } = props;
columns : Column<BlockedIPEntry>[] = [ const canUnblockBlockedIPAddress =
{ key: "ipAddress", label: "IP Address", order: "asc" }, authentication.hasAccess("UnlockIPAddress");
{ key: "numberOfAttempts", label: "Number of Attempts", order: "asc", searchable: false },
{ key: "blockedAt", label: "Date", order: "asc", searchable: false }, const columns: Column<BlockedIPEntry>[] = useMemo(
{ key: "unblockedIn", label: "Unblocked In (Minutes)", order: "asc", searchable: false }, () => [
{ key: "ipAddress", label: t("IPAddress"), order: "asc" },
{ {
key: "action", label: "", searchable: false, content: (item) => { key: "numberOfAttempts",
if (this.canUnblockBlockedIPAddress) return <><ConfirmButton buttonType={ButtonType.primary} keyValue={item} onClick={this.props.onDelete} confirmMessage={"Press again to unblock"} ><FontAwesomeIcon icon={faUnlock} /></ConfirmButton></>; return (<></>) label: t("NumberOfAttempts"),
} order: "asc",
} searchable: false,
]; },
{ key: "blockedAt", label: t("Date"), order: "asc", searchable: false },
{
key: "unblockedIn",
label: t("UnblockedInMinutes"),
order: "asc",
searchable: false,
},
{
key: "action",
label: "",
searchable: false,
content: (item) =>
canUnblockBlockedIPAddress ? (
<ConfirmButton
buttonType={ButtonType.primary}
keyValue={item}
onClick={onDelete}
confirmMessage={t("PressAgainToUnblock")}
>
<FontAwesomeIcon icon={faUnlock} />
</ConfirmButton>
) : (
<></>
),
},
],
[canUnblockBlockedIPAddress, onDelete, t],
);
raiseSort = (sortColumn : Column<BlockedIPEntry>) => { const raiseSort = useCallback(
this.setState({sortColumn}); (sortCol: Column<BlockedIPEntry>) => {
if (this.props.onSort !== undefined) if (onSort !== undefined) onSort(sortCol);
this.props.onSort(sortColumn); },
} [onSort],
);
render() { return (
const { data, sortColumn, onChangePage, onSearch } = this.props; <Table
data={data}
return <Table data={data} keyName="ipAddress" columns={this.columns} sortColumn={sortColumn} onSort={this.raiseSort} onChangePage={onChangePage} onSearch={onSearch}/>; keyName="ipAddress"
columns={columns}
sortColumn={sortColumn}
onSort={raiseSort}
onChangePage={onChangePage}
onSearch={onSearch}
/>
);
} }
}
export default BlockedIPsTable;

View File

@ -1,27 +1,122 @@
import React from "react"; import React, { useMemo } from "react";
import Column from "../../../components/common/columns"; import Column from "../../../components/common/columns";
import Table, { PublishedTableProps } from "../../../components/common/Table"; import Table, { PublishedTableProps } from "../../../components/common/Table";
import { ErrorLog } from "../services/errorLogsService"; import { ErrorLog } from "../services/errorLogsService";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../../i18n/i18n";
class ErrorLogsTable extends React.Component<PublishedTableProps<ErrorLog>> { import ExpandableCell from "../../../components/common/ExpandableCell";
columns: Column<ErrorLog>[] = [ import { max } from "date-fns";
{ key: "id", label: "Id", order: "asc" },
{ key: "application", label: "Application", order: "asc" },
{ key: "message", label: "Message", order: "asc" },
{ key: "occuredAt", label: "Occured At", order: "asc", searchable: false }
];
raiseSort = (sortColumn: Column<ErrorLog>) => { export default function ErrorLogsTable(
this.setState({ sortColumn }); props: PublishedTableProps<ErrorLog>,
if (this.props.onSort !== undefined) ): JSX.Element {
this.props.onSort(sortColumn); const { t } = useTranslation<typeof Namespaces.Common>();
} const { data, sortColumn, onChangePage, onSearch, onSort } = props;
render() { const columns: Column<ErrorLog>[] = useMemo(
const { data, sortColumn, onChangePage, onSearch } = this.props; () => [
{ key: "id", label: t("Id"), order: "asc" },
return <Table data={data} keyName="id" columns={this.columns} sortColumn={sortColumn} onSort={this.raiseSort} onChangePage={onChangePage} onSearch={onSearch} />; { key: "application", label: t("Application"), order: "asc" },
{ key: "message", label: t("Message"), order: "asc" },
{
key: "occuredAt",
label: t("OccuredAt"),
order: "asc",
searchable: false,
},
{
key: "exceptionJson",
label: t("ExceptionJson"),
order: "asc",
searchable: false,
content: (item: ErrorLog) => {
const raw = item.exceptionJson ?? "";
try {
const parsed = JSON.parse(raw);
const pretty = JSON.stringify(parsed, null, 2)
.replace(/\\r\\n/g, "\n")
.replace(/\\n/g, "\n");
return (
<ExpandableCell
title={t("ShowJSON")}
contentClassName="errorlogs-ExceptionJson"
>
<pre className="errorlogs-pre">{pretty}</pre>
</ExpandableCell>
);
} catch {
const safe = raw.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n");
return (
<ExpandableCell title={t("ShowJSON")}>
<pre className="errorlogs-pre">{safe}</pre>
</ExpandableCell>
);
} }
},
},
//cjd- hiding the stack trace as this information is already availalbe in the exceptionJson field
// {
// key: "stackTrace",
// label: t("StackTrace"),
// order: "asc",
// searchable: false,
// content: (item: ErrorLog) => (
// <ExpandableCell
// title={t("ShowStackTrace")}
// contentClassName="errorlogs-StackTraceJson"
// >
// <pre className="errorlogs-pre">{item.stackTrace ?? ""}</pre>
// </ExpandableCell>
// ),
// },
{
key: "supportingData",
label: t("SupportingData"),
order: "asc",
searchable: false,
content: (item: ErrorLog) => {
const raw = item.supportingData ?? "";
try {
const parsed = JSON.parse(raw);
const pretty = JSON.stringify(parsed, null, 2)
.replace(/\\r\\n/g, "\n")
.replace(/\\n/g, "\n");
return (
<ExpandableCell
title={t("ShowJSON")}
contentClassName="errorlogs-SupportingData"
>
<pre className="errorlogs-pre">{pretty}</pre>
</ExpandableCell>
);
} catch {
const safe = raw.replace(/\\r\\n/g, "\n").replace(/\\n/g, "\n");
return (
<ExpandableCell title={t("ShowJSON")}>
<pre className="errorlogs-pre">{safe}</pre>
</ExpandableCell>
);
} }
},
},
],
[t],
);
export default ErrorLogsTable; const raiseSort = (sortColumnParam: Column<ErrorLog>) => {
if (onSort) onSort(sortColumnParam);
};
return (
<Table
data={data}
keyName="id"
columns={columns}
sortColumn={sortColumn}
onSort={raiseSort}
onChangePage={onChangePage}
onSearch={onSearch}
/>
);
}

View File

@ -1,72 +1,101 @@
import { Component } from 'react'; import { useCallback, useEffect, useState } from "react";
import Column from '../../components/common/columns'; import Column from "../../components/common/columns";
import { Paginated } from '../../services/Paginated'; import { Paginated } from "../../services/Paginated";
import ErrorLogsTable from './components/errorLogsTable'; import ErrorLogsTable from "./components/errorLogsTable";
import errorLogsService, { ErrorLog } from './services/errorLogsService'; import errorLogsService, { ErrorLog } from "./services/errorLogsService";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../i18n/i18n";
interface ErrorLogState { const ErrorLogs = (): JSX.Element => {
pagedData: Paginated<ErrorLog>, const { t } = useTranslation<typeof Namespaces.Common>();
sortColumn: Column<ErrorLog>, const [pagedData, setPagedData] = useState<Paginated<ErrorLog>>({
filters: Map<string, string>;
}
class ErrorLogs extends Component<any, any, ErrorLogState> {
state = {
pagedData: {
page: 1, page: 1,
pageSize: 10, pageSize: 10,
count: 0, count: 0,
totalPages: 1, totalPages: 1,
data: [] data: [],
});
const [sortColumn, setSortColumn] = useState<Column<ErrorLog>>({
key: "Id",
label: t("Id"),
order: "desc",
});
const [filters, setFilters] = useState<Map<string, string>>(
new Map<string, string>(),
);
const loadErrorLogs = useCallback(
async (
page: number,
pageSize: number,
column: Column<ErrorLog>,
filterMap: Map<string, string>,
) => {
const data = await errorLogsService.getErrorLogs(
page,
pageSize,
column.key,
column.order === "asc",
filterMap,
);
if (data) {
setPagedData(data);
}
}, },
sortColumn: { key: "Id", label: "Id", order: "desc" }, [],
filters: new Map<string, string>() );
}
componentDidMount = async () => { useEffect(() => {
const { page, pageSize } = this.state.pagedData; loadErrorLogs(pagedData.page, pagedData.pageSize, sortColumn, filters);
}, [loadErrorLogs]);
await this.changePage(page, pageSize); const changePage = useCallback(
} async (page: number, pageSize: number) => {
await loadErrorLogs(page, pageSize, sortColumn, filters);
},
[loadErrorLogs, sortColumn, filters],
);
changePage = async (page: number, pageSize: number) => { const onSort = useCallback(
const { sortColumn, filters } = this.state; async (nextSortColumn: Column<ErrorLog>) => {
setSortColumn(nextSortColumn);
await loadErrorLogs(
pagedData.page,
pagedData.pageSize,
nextSortColumn,
filters,
);
},
[loadErrorLogs, pagedData.page, pagedData.pageSize, filters],
);
const pagedData = await errorLogsService.getErrorLogs(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); const onSearch = useCallback(
if (pagedData) { async (name: string, value: string) => {
this.setState({ pagedData }); const nextFilters = new Map(filters);
} nextFilters.set(name, value);
} setFilters(nextFilters);
onSort = async (sortColumn: Column<ErrorLog>) => { await loadErrorLogs(
const { page, pageSize } = this.state.pagedData; pagedData.page,
const { filters } = this.state; pagedData.pageSize,
const pagedData = await errorLogsService.getErrorLogs(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters); sortColumn,
if (pagedData) { nextFilters,
this.setState({ pagedData, sortColumn }); );
} },
} [loadErrorLogs, pagedData.page, pagedData.pageSize, sortColumn, filters],
);
onSearch = async (name: string, value: string) => {
const { page, pageSize } = this.state.pagedData;
const { sortColumn, filters } = this.state;
filters.set(name, value);
const pagedData = await errorLogsService.getErrorLogs(page, pageSize, sortColumn.key, sortColumn.order === "asc", filters);
if (pagedData) {
this.setState({ filters, pagedData });
}
};
render(): JSX.Element {
const { pagedData, sortColumn } = this.state;
return ( return (
<div> <div>
<ErrorLogsTable data={pagedData} sortColumn={sortColumn} onChangePage={this.changePage} onSort={this.onSort} onSearch={this.onSearch} /> <ErrorLogsTable
data={pagedData}
sortColumn={sortColumn}
onChangePage={changePage}
onSort={onSort}
onSearch={onSearch}
/>
</div> </div>
); );
}
}; };
export default ErrorLogs; export default ErrorLogs;

View File

@ -4,31 +4,42 @@ import MapToJson from "../../../utils/MapToJson";
const apiEndpoint = "/exceptionlogs"; const apiEndpoint = "/exceptionlogs";
export type ErrorLog = {
export type ErrorLog =
{
id: bigint; id: bigint;
application: string; application: string;
aessage: string; aessage: string;
occuredAt: Date; occuredAt: Date;
} stackTrace: string;
supportingData: string;
exceptionJson: string;
};
export async function getErrorLogs(page: number, pageSize: number, sortKey: string, sortAscending: boolean, filters?: Map<string, string>): Promise<Paginated<ErrorLog>> { export async function getErrorLogs(
page: number,
pageSize: number,
sortKey: string,
sortAscending: boolean,
filters?: Map<string, string>,
): Promise<Paginated<ErrorLog>> {
const filterString = MapToJson(filters); const filterString = MapToJson(filters);
const response = await httpService.get<Paginated<ErrorLog>>(apiEndpoint + "/exceptionlogs", const response = await httpService.get<Paginated<ErrorLog>>(
{ params: { apiEndpoint + "/exceptionlogs",
{
params: {
page: page, page: page,
pageSize: pageSize, pageSize: pageSize,
sortKey: sortKey, sortKey: sortKey,
sortAscending: sortAscending, sortAscending: sortAscending,
filters : filterString filters: filterString,
} } ); },
},
);
return response?.data; return response?.data;
} }
const errorLogsService = { const errorLogsService = {
getErrorLogs getErrorLogs,
}; };
export default errorLogsService; export default errorLogsService;