diff --git a/package.json b/package.json index 313f66a..355f19f 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,10 @@ "dependencies": { "@data-driven-forms/pf4-component-mapper": "2.21.1", "@data-driven-forms/react-form-renderer": "2.21.1", - "@patternfly/patternfly": "4.87.1", - "@patternfly/react-core": "4.97.0", - "@patternfly/react-table": "4.23.0", + "@konveyor/lib-ui": "^2.0.0", + "@patternfly/patternfly": "4.90.5", + "@patternfly/react-core": "4.101.2", + "@patternfly/react-table": "4.23.13", "@react-keycloak/web": "^3.4.0", "@redhat-cloud-services/frontend-components-notifications": "2.2.3", "@testing-library/jest-dom": "^5.11.4", @@ -30,14 +31,18 @@ "jest-enzyme": "^7.1.2", "keycloak-js": "^12.0.1", "lint-staged": "^10.4.2", + "lodash": "^4.17.21", + "moment": "^2.29.1", "prettier": "^2.1.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-dropzone": "^11.3.1", + "react-moment": "^1.1.1", "react-redux": "^7.2.1", "react-router-dom": "^5.2.0", "react-scripts": "4.0.0", "react-test-renderer": "^16.13.1", + "react-use-websocket": "^2.5.0", "redux": "^4.0.5", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", @@ -112,6 +117,7 @@ "@testing-library/react-hooks": "^3.4.2", "@types/enzyme": "^3.10.7", "@types/enzyme-adapter-react-16": "^1.0.6", + "@types/lodash": "^4.14.168", "@types/react-redux": "^7.1.9", "@types/react-router-dom": "^5.1.6", "@types/redux-logger": "^3.0.8", diff --git a/src/api/models.tsx b/src/api/models.tsx index 1622ff5..766babd 100644 --- a/src/api/models.tsx +++ b/src/api/models.tsx @@ -1,3 +1,16 @@ +export type EntityEventType = "CREATED" | "UPDATED" | "DELETED"; + +export interface WsMessage { + type: "event"; + spec: EntityEvent; +} + +export interface EntityEvent { + id: string; + event: EntityEventType; + entity: string; +} + export interface PageQuery { page: number; perPage: number; @@ -45,15 +58,43 @@ export interface SUNATCredentials { password?: string; } +export type DeliveryStatus = + | "SCHEDULED_TO_DELIVER" + | "NEED_TO_CHECK_TICKET" + | "COULD_NOT_BE_DELIVERED" + | "DELIVERED"; + export interface UBLDocument { id?: string; - deliveryStatus: string; - fileInfo: FileInfo; + createdOn: number; + + retries: number; + willRetryOn: number; + + fileContentValid?: boolean; + fileContentValidationError?: string; + fileContent?: UBLDocumentFileContent; + + sunat?: UBLDocumentSunat; + sunatDeliveryStatus: DeliveryStatus; + sunatEvents: UBLDocumentEvent[]; } -export interface FileInfo { +export interface UBLDocumentFileContent { + ruc: string; documentID: string; documentType: string; - filename: string; - ruc: string; +} + +export interface UBLDocumentSunat { + code: string; + status: "ACEPTADO" | "RECHAZADO" | "EXCEPCION" | "BAJA" | "EN_PROCESO"; + description: string; + ticket: string; +} + +export interface UBLDocumentEvent { + status: "default" | "success" | "danger" | "warning" | "info"; + description: string; + createdOn: number; } diff --git a/src/images/sunat.png b/src/images/sunat.png new file mode 100644 index 0000000..22142e1 Binary files /dev/null and b/src/images/sunat.png differ diff --git a/src/pages/companies/company-list/company-list.tsx b/src/pages/companies/company-list/company-list.tsx index faecbf8..f88751a 100644 --- a/src/pages/companies/company-list/company-list.tsx +++ b/src/pages/companies/company-list/company-list.tsx @@ -1,6 +1,8 @@ import React, { useCallback, useEffect, useState } from "react"; import { Link, RouteComponentProps } from "react-router-dom"; import { useDispatch } from "react-redux"; +import useWebSocket from "react-use-websocket"; +import { useKeycloak } from "@react-keycloak/web"; import { Bullseye, @@ -16,9 +18,11 @@ import { ToolbarItem, } from "@patternfly/react-core"; import { + cellWidth, IActions, ICell, IExtraData, + IRow, IRowData, sortable, } from "@patternfly/react-table"; @@ -43,7 +47,7 @@ import { DeleteWithMatchModalContainer } from "shared/containers"; import { formatPath, Paths } from "Paths"; import { CompanySortBy, CompanySortByQuery } from "api/rest"; -import { Company, SortByQuery } from "api/models"; +import { Company, WsMessage, SortByQuery } from "api/models"; import { getAxiosErrorMessage } from "utils/modelUtils"; import { Welcome } from "./components/welcome"; @@ -80,6 +84,7 @@ export interface CompanyListProps extends RouteComponentProps {} export const CompanyList: React.FC = ({ history }) => { const dispatch = useDispatch(); + const { keycloak } = useKeycloak(); const [filterText, setFilterText] = useState(""); @@ -120,13 +125,54 @@ export const CompanyList: React.FC = ({ history }) => { ); }, [filterText, paginationQuery, sortByQuery, fetchCompanies]); + const socketUrl = "ws://localhost:8080/companies"; + + const { + lastJsonMessage: eventMsg, + sendJsonMessage: sendEventMessage, + } = useWebSocket(socketUrl, { + onOpen: () => { + sendEventMessage({ + authentication: { + token: keycloak.token, + }, + }); + }, + shouldReconnect: (event: CloseEvent) => event.code !== 1011, + share: true, + }); + + useEffect(() => { + if (eventMsg) { + const event: WsMessage = eventMsg as WsMessage; + + switch (event.spec.event) { + case "CREATED": + if ( + paginationQuery.page === 1 && + !sortByQuery && + !(companies?.data || []).find((f) => f.id === event.spec.id) + ) { + refreshTable(); + } + break; + case "DELETED": + if (companies && companies.data.find((f) => f.id === event.spec.id)) { + refreshTable(); + } + break; + } + } + }, [eventMsg, companies, paginationQuery, sortByQuery, refreshTable]); + const columns: ICell[] = [ - { title: "Name", transforms: [sortable] }, + { title: "Name", transforms: [sortable, cellWidth(40)] }, { title: "Description" }, ]; - const itemsToRow = (items: Company[]) => { - return items.map((item) => ({ + const rows: IRow[] = []; + companies?.data.forEach((item) => { + rows.push({ [COMPANY_FIELD]: item, cells: [ { @@ -140,8 +186,8 @@ export const CompanyList: React.FC = ({ history }) => { title: item.description, }, ], - })); - }; + }); + }); const actions: IActions = [ { @@ -176,7 +222,7 @@ export const CompanyList: React.FC = ({ history }) => { row, () => { dispatch(deleteWithMatchModalActions.closeModal()); - refreshTable(); + // refreshTable(); }, (error) => { dispatch(deleteWithMatchModalActions.closeModal()); @@ -222,16 +268,15 @@ export const CompanyList: React.FC = ({ history }) => { 0} toolbarToggle={ @@ -240,18 +285,20 @@ export const CompanyList: React.FC = ({ history }) => { } toolbar={ - - - - - + <> + + + + + + } noDataState={ diff --git a/src/pages/companies/new-company/new-company.tsx b/src/pages/companies/new-company/new-company.tsx index 82cf703..e89982f 100644 --- a/src/pages/companies/new-company/new-company.tsx +++ b/src/pages/companies/new-company/new-company.tsx @@ -1,7 +1,15 @@ import React, { useState } from "react"; import { RouteComponentProps } from "react-router-dom"; -import { Alert, PageSection, Stack, StackItem } from "@patternfly/react-core"; +import { + Alert, + Button, + Flex, + FlexItem, + PageSection, + Stack, + StackItem, +} from "@patternfly/react-core"; import FormRenderer from "@data-driven-forms/react-form-renderer/dist/cjs/form-renderer"; import Pf4FormTemplate from "@data-driven-forms/pf4-component-mapper/dist/cjs/form-template"; @@ -30,11 +38,44 @@ export interface NewCompanyFormValues { }; } +const BETA_TEMPLATE: NewCompanyFormValues = { + name: "", + description: "This is a test company", + credentials: { + username: "12345678959MODDATOS", + password: "MODDATOS", + }, + webServices: { + factura: "https://e-beta.sunat.gob.pe/ol-ti-itcpfegem-beta/billService", + guia: + "https://e-beta.sunat.gob.pe/ol-ti-itemision-guia-gem-beta/billService", + retenciones: + "https://e-beta.sunat.gob.pe/ol-ti-itemision-otroscpe-gem-beta/billService", + }, +}; + +const PROD_TEMPLATE: NewCompanyFormValues = { + name: "", + description: "", + credentials: { + username: "", + password: "", + }, + webServices: { + factura: "https://e-factura.sunat.gob.pe/ol-ti-itcpfegem/billService", + guia: + "https://e-guiaremision.sunat.gob.pe/ol-ti-itemision-guia-gem/billService", + retenciones: + "https://e-factura.sunat.gob.pe/ol-ti-itemision-otroscpe-gem/billService", + }, +}; + export interface CompanyListProps extends RouteComponentProps {} export const NewCompany: React.FC = ({ history }) => { const dispatch = useDispatch(); const [conflictErrorMsg, setConflictErrorMsg] = useState(""); + const [initialValues, setInitialValues] = useState(); const handleOnSubmit = (formValues: any) => { return createCompany(formValues) @@ -57,9 +98,31 @@ export const NewCompany: React.FC = ({ history }) => { history.push(Paths.companyList); }; + const applyBetaTemplate = () => { + setInitialValues(BETA_TEMPLATE); + }; + + const applyProdTemplate = () => { + setInitialValues(PROD_TEMPLATE); + }; + return ( + + + + + + + + + + {conflictErrorMsg && ( @@ -67,6 +130,7 @@ export const NewCompany: React.FC = ({ history }) => { )} ( diff --git a/src/pages/documents/document-list/document-list.tsx b/src/pages/documents/document-list/document-list.tsx index 11db4da..7461971 100644 --- a/src/pages/documents/document-list/document-list.tsx +++ b/src/pages/documents/document-list/document-list.tsx @@ -1,41 +1,74 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { RouteComponentProps, useHistory, useParams } from "react-router-dom"; +import { StatusIcon, StatusType } from "@konveyor/lib-ui"; +import Moment from "react-moment"; + +import { useKeycloak } from "@react-keycloak/web"; +import useWebSocket from "react-use-websocket"; import { Button, ButtonVariant, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, EmptyState, EmptyStateBody, EmptyStateIcon, EmptyStateVariant, + NotificationDrawer, + NotificationDrawerBody, + NotificationDrawerList, + NotificationDrawerListItem, + NotificationDrawerListItemBody, + NotificationDrawerListItemHeader, PageSection, Title, ToolbarGroup, ToolbarItem, } from "@patternfly/react-core"; import { - IActions, + cellWidth, + compoundExpand, ICell, IExtraData, + IRow, IRowData, - sortable, + TableText, } from "@patternfly/react-table"; -import { AddCircleOIcon } from "@patternfly/react-icons"; +import { + AddCircleOIcon, + FileCodeIcon, + BullseyeIcon, +} from "@patternfly/react-icons"; +import { DeleteWithMatchModalContainer } from "shared/containers"; import { AppPlaceholder, AppTableWithControls, ConditionalRender, + DocumentStatus, SearchFilter, SimplePageSection, } from "shared/components"; -import { useTableControls, useFetchDocuments } from "shared/hooks"; -import { DeleteWithMatchModalContainer } from "shared/containers"; +import { + useTableControls, + useFetchDocuments, + useColSelectionState, +} from "shared/hooks"; import { CompanytRoute, formatPath, Paths } from "Paths"; -import { UBLDocument, SortByQuery } from "api/models"; +import { + UBLDocument, + SortByQuery, + UBLDocumentSunat, + WsMessage, +} from "api/models"; import { UBLDocumentSortBy, UBLDocumentSortByQuery } from "api/rest"; +import sunatLogo from "images/sunat.png"; + const toSortByQuery = ( sortBy?: SortByQuery ): UBLDocumentSortByQuery | undefined => { @@ -58,10 +91,32 @@ const toSortByQuery = ( }; }; -const DOCUMENT_FIELD = "document"; +const ENTITY_FIELD = "entity"; const getRow = (rowData: IRowData): UBLDocument => { - return rowData[DOCUMENT_FIELD]; + return rowData[ENTITY_FIELD]; +}; + +const getStatusType = (ublDocument: UBLDocumentSunat): StatusType => { + switch (ublDocument.status) { + case "ACEPTADO": + return "Ok"; + case "RECHAZADO": + return "Error"; + case "EXCEPCION": + return "Error"; + case "BAJA": + return "Warning"; + case "EN_PROCESO": + return "Warning"; + default: + return "Unknown"; + } +}; + +const formatSunatStatus = (status: string) => { + const withSpace = status.replace(/_/g, " ").toLowerCase(); + return withSpace.charAt(0).toUpperCase() + withSpace.slice(1); }; export interface DocumentListProps extends RouteComponentProps {} @@ -69,6 +124,7 @@ export interface DocumentListProps extends RouteComponentProps {} export const DocumentList: React.FC = () => { const history = useHistory(); const params = useParams(); + const { keycloak } = useKeycloak(); const [filterText, setFilterText] = useState(""); @@ -76,7 +132,7 @@ export const DocumentList: React.FC = () => { documents, isFetching, fetchError, - fetchDocuments, + fetchDocumentsStream, } = useFetchDocuments(true); const { @@ -86,19 +142,19 @@ export const DocumentList: React.FC = () => { handleSortChange, } = useTableControls(); - // const refreshTable = useCallback(() => { - // fetchDocuments( - // params.company, - // { - // filterText, - // }, - // paginationQuery, - // toSortByQuery(sortByQuery) - // ); - // }, [filterText, paginationQuery, sortByQuery, fetchDocuments]); + const refreshTable = useCallback(() => { + fetchDocumentsStream( + params.company, + { + filterText, + }, + paginationQuery, + toSortByQuery(sortByQuery) + ); + }, [params, filterText, paginationQuery, sortByQuery, fetchDocumentsStream]); useEffect(() => { - fetchDocuments( + fetchDocumentsStream( params.company, { filterText, @@ -106,61 +162,295 @@ export const DocumentList: React.FC = () => { paginationQuery, toSortByQuery(sortByQuery) ); - }, [params, filterText, paginationQuery, sortByQuery, fetchDocuments]); + }, [params, filterText, paginationQuery, sortByQuery, fetchDocumentsStream]); + + const [socketUrl, setSocketUrl] = useState( + `ws://localhost:8080/companies/${params.company}/documents` + ); + useEffect(() => { + if (params.company) { + setSocketUrl(`ws://localhost:8080/companies/${params.company}/documents`); + } + }, [params]); + + const { + lastJsonMessage: eventMsg, + sendJsonMessage: sendEventMessage, + } = useWebSocket(socketUrl, { + onOpen: () => { + sendEventMessage({ + authentication: { + token: keycloak.token, + }, + }); + }, + shouldReconnect: (event: CloseEvent) => event.code !== 1011, + share: true, + }); + + useEffect(() => { + if (eventMsg) { + const event: WsMessage = eventMsg as WsMessage; + + switch (event.spec.event) { + case "CREATED": + if ( + paginationQuery.page === 1 && + !sortByQuery && + !(documents?.data || []).find((f) => f.id === event.spec.id) + ) { + refreshTable(); + } + break; + case "DELETED": + if (documents && documents.data.find((f) => f.id === event.spec.id)) { + refreshTable(); + } + break; + } + } + }, [eventMsg, documents, paginationQuery, sortByQuery, refreshTable]); + + // const columns: ICell[] = [ - { title: "Ruc" }, - { title: "ID", transforms: [sortable] }, - { title: "Type" }, - { title: "Status" }, + { + title: "Id", + cellTransforms: [], + transforms: [cellWidth(15)], + }, + { + title: "XML", + cellTransforms: [compoundExpand], + transforms: [cellWidth(40)], + }, + { title: "SUNAT", cellTransforms: [compoundExpand], transforms: [] }, + { title: "Events", cellTransforms: [compoundExpand], transforms: [] }, + { title: "Created on", cellTransforms: [] }, ]; - const itemsToRow = (items: UBLDocument[]) => { - return items.map((item) => ({ - [DOCUMENT_FIELD]: item, + const { + isColSelected: isColumnExpanded, + toggleSelectedColSingle: toggleColumnExpanded, + } = useColSelectionState({ + rows: documents?.data || [], + isEqual: (a, b) => a.id === b.id, + }); + + const rows: IRow[] = []; + documents?.data.forEach((item) => { + const is2ndColumnExpanded = isColumnExpanded(item, 1); + const is3rdColumnExpanded = isColumnExpanded(item, 2); + const is4thColumnExpanded = isColumnExpanded(item, 3); + + rows.push({ + [ENTITY_FIELD]: item, + isOpen: is2ndColumnExpanded && is3rdColumnExpanded && is4thColumnExpanded, cells: [ { - title: item.fileInfo.ruc, + title: ( + + {(item.sunatDeliveryStatus === "SCHEDULED_TO_DELIVER" || + item.sunatDeliveryStatus === "NEED_TO_CHECK_TICKET") && ( + <> + +   + + )} + {item.sunatDeliveryStatus === "DELIVERED" && ( + <> + SUNAT logo +   + + )} + {item.id} + + ), }, { - title: item.fileInfo.documentID, + props: { + isOpen: is2ndColumnExpanded, + }, + title: ( + <> + {item.fileContentValid === true && item.fileContent && ( + <> + {" "} + {item.fileContent?.documentID} + + )} + {item.fileContentValid === false && ( + + )} + + ), }, { - title: item.fileInfo.documentType, + props: { + isOpen: is3rdColumnExpanded, + }, + title: ( + <> + {item.sunat && item.sunat.status && ( + + )} + {item.sunat && !item.sunat.status && ( + + )} + + ), }, { - title: item.deliveryStatus, + props: { + isOpen: is4thColumnExpanded, + }, + title: ( + <> + {item.sunatEvents.length} + + ), + }, + { + title: {item.createdOn}, }, ], - })); + }); + + if (is2ndColumnExpanded) { + rows.push({ + parent: rows.length - 1, + compoundParent: 1, + cells: [ + { + title: ( + + + Document type + + {item.fileContent?.documentType} + + + + ID + + {item.fileContent?.documentID} + + + + RUC + + {item.fileContent?.ruc} + + + + ), + }, + ], + }); + } + if (is3rdColumnExpanded) { + rows.push({ + parent: rows.length - 1, + compoundParent: 2, + cells: [ + { + title: ( + + + Status + + {item.sunat?.status} + + + + Code + + {item.sunat?.code} + + + {item.sunat?.ticket && ( + + Ticket + + {item.sunat?.description} + + + )} + + Description + + {item.sunat?.description} + + + + ), + }, + ], + }); + } + if (is4thColumnExpanded) { + rows.push({ + parent: rows.length - 1, + compoundParent: 3, + noPadding: true, + cells: [ + { + title: ( + + + + {item.sunatEvents.map((event, index) => ( + + + {item.createdOn}} + > + + ))} + + + + ), + }, + ], + }); + } + }); + + // Rows + + const onExpandColumn = ( + event: React.MouseEvent, + rowIndex: number, + colIndex: number, + isOpen: boolean, + rowData: IRowData, + extraData: IExtraData + ) => { + const row = getRow(rowData); + toggleColumnExpanded(row, colIndex); }; - const actions: IActions = [ - { - title: "Edit", - onClick: ( - event: React.MouseEvent, - rowIndex: number, - rowData: IRowData, - extraData: IExtraData - ) => { - const row: UBLDocument = getRow(rowData); - console.log(row); - }, - }, - { - title: "Delete", - onClick: ( - event: React.MouseEvent, - rowIndex: number, - rowData: IRowData, - extraData: IExtraData - ) => { - const row: UBLDocument = getRow(rowData); - console.log(row); - }, - }, - ]; + // const handleOnNewCompany = () => { history.push(formatPath(Paths.newDocument, { company: params.company })); @@ -181,16 +471,15 @@ export const DocumentList: React.FC = () => { 0} toolbarToggle={ diff --git a/src/shared/components/app-table-with-controls/app-table-with-controls.tsx b/src/shared/components/app-table-with-controls/app-table-with-controls.tsx index 55ac66f..c3b00d6 100644 --- a/src/shared/components/app-table-with-controls/app-table-with-controls.tsx +++ b/src/shared/components/app-table-with-controls/app-table-with-controls.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React from "react"; import { Toolbar, @@ -15,6 +15,8 @@ import { IExtraColumnData, IRow, ISortBy, + OnCollapse, + OnExpand, SortByDirection, } from "@patternfly/react-table"; import { FilterIcon } from "@patternfly/react-icons"; @@ -24,8 +26,6 @@ import { SimplePagination } from "../simple-pagination"; export interface AppTableWithControlsProps { count: number; - items: any[]; - itemsToRow: (items: any[]) => IRow[]; pagination: { perPage?: number; @@ -46,6 +46,10 @@ export interface AppTableWithControlsProps { extraData: IExtraColumnData ) => void; + onCollapse?: OnCollapse; + onExpand?: OnExpand; + + rows: IRow[]; columns: ICell[]; actions?: IActions; actionResolver?: IActionsResolver; @@ -67,14 +71,16 @@ export interface AppTableWithControlsProps { export const AppTableWithControls: React.FC = ({ count, - items, - itemsToRow, pagination, sortBy, handlePaginationChange, handleSortChange, + onCollapse, + onExpand, + + rows, columns, actions, actionResolver, @@ -93,8 +99,6 @@ export const AppTableWithControls: React.FC = ({ noSearchResultsState, errorState, }) => { - const rows = useMemo(() => itemsToRow(items), [items, itemsToRow]); - return (
= ({ loadingVariant={loadingVariant} sortBy={sortBy} onSort={handleSortChange} + onCollapse={onCollapse} + onExpand={onExpand} filtersApplied={filtersApplied} noDataState={noDataState} noSearchResultsState={noSearchResultsState} diff --git a/src/shared/components/app-table/app-table.tsx b/src/shared/components/app-table/app-table.tsx index 57e65b5..f7249b6 100644 --- a/src/shared/components/app-table/app-table.tsx +++ b/src/shared/components/app-table/app-table.tsx @@ -11,6 +11,8 @@ import { IActionsResolver, IAreActionsDisabled, OnSort, + OnCollapse, + OnExpand, } from "@patternfly/react-table"; import { StateNoData } from "./state-no-data"; @@ -31,6 +33,9 @@ export interface AppTableProps { sortBy?: ISortBy; onSort?: OnSort; + onCollapse?: OnCollapse; + onExpand?: OnExpand; + filtersApplied: boolean; noDataState?: any; noSearchResultsState?: any; @@ -50,6 +55,8 @@ export const AppTable: React.FC = ({ loadingVariant = "skeleton", sortBy, onSort, + onCollapse, + onExpand, filtersApplied, noDataState, noSearchResultsState, @@ -135,6 +142,8 @@ export const AppTable: React.FC = ({ areActionsDisabled={areActionsDisabled} sortBy={sortBy} onSort={onSort} + onCollapse={onCollapse} + onExpand={onExpand} > diff --git a/src/shared/components/document-status/document-status.tsx b/src/shared/components/document-status/document-status.tsx new file mode 100644 index 0000000..b8d41d4 --- /dev/null +++ b/src/shared/components/document-status/document-status.tsx @@ -0,0 +1,107 @@ +import React from "react"; + +import { Flex, FlexItem, SpinnerProps } from "@patternfly/react-core"; +import { + CircleIcon, + CheckCircleIcon, + ExclamationCircleIcon, + InfoCircleIcon, + QuestionCircleIcon, +} from "@patternfly/react-icons"; +import { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon"; +import { + global_disabled_color_200 as disabledColor, + global_success_color_100 as successColor, + global_warning_color_100 as warningColor, + global_Color_dark_200 as unknownColor, + global_danger_color_100 as errorColor, + global_info_color_100 as infoColor, +} from "@patternfly/react-tokens"; + +import { GhSpinner } from "../gh-spinner"; + +export type StatusType = + | "Scheduled" + | "InProgress" + | "Success" + | "Error" + | "Info" + | "Unknown"; + +type IconListType = { + [key in StatusType]: { + Icon: + | React.ComponentClass + | React.FunctionComponent; + color: { name: string; value: string; var: string }; + }; +}; +const iconList: IconListType = { + Scheduled: { + Icon: CircleIcon, + color: warningColor, + }, + InProgress: { + Icon: GhSpinner, + color: warningColor, + }, + Success: { + Icon: CheckCircleIcon, + color: successColor, + }, + Error: { + Icon: ExclamationCircleIcon, + color: errorColor, + }, + Info: { + Icon: InfoCircleIcon, + color: infoColor, + }, + Unknown: { + Icon: QuestionCircleIcon, + color: unknownColor, + }, +}; + +export interface DocumentStatusProps { + status: StatusType; + label?: React.ReactNode; + isDisabled?: boolean; + className?: string; +} + +export const DocumentStatus: React.FC = ({ + status, + label, + isDisabled = false, + className = "", +}) => { + const Icon = iconList[status].Icon; + const icon = ( + + ); + + if (label) { + return ( + + {icon} + {label} + + ); + } + + return icon; +}; diff --git a/src/shared/components/document-status/index.ts b/src/shared/components/document-status/index.ts new file mode 100644 index 0000000..d1dda07 --- /dev/null +++ b/src/shared/components/document-status/index.ts @@ -0,0 +1 @@ +export { DocumentStatus } from "./document-status"; diff --git a/src/shared/components/document-status/stories/document-status.stories.tsx b/src/shared/components/document-status/stories/document-status.stories.tsx new file mode 100644 index 0000000..1129083 --- /dev/null +++ b/src/shared/components/document-status/stories/document-status.stories.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { Story, Meta } from "@storybook/react/types-6-0"; +import { DocumentStatus, DocumentStatusProps } from "../document-status"; + +export default { + title: "Components / DocumentStatus", + component: DocumentStatus, +} as Meta; + +const Template: Story = (args) => ( + +); + +export const Scheduled = Template.bind({}); +Scheduled.args = { + status: "Scheduled", +}; + +export const InProgress = Template.bind({}); +InProgress.args = { + status: "InProgress", +}; + +export const Success = Template.bind({}); +Success.args = { + status: "Success", +}; + +export const Error = Template.bind({}); +Error.args = { + status: "Error", +}; diff --git a/src/shared/components/document-status/tests/document-status.test.tsx b/src/shared/components/document-status/tests/document-status.test.tsx new file mode 100644 index 0000000..b0b40ab --- /dev/null +++ b/src/shared/components/document-status/tests/document-status.test.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { DocumentStatus } from "../document-status"; + +describe("DocumentStatus", () => { + it("Renders without crashing", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/shared/components/gh-spinner/gh-spinner-icon.tsx b/src/shared/components/gh-spinner/gh-spinner-icon.tsx new file mode 100644 index 0000000..8b3a59e --- /dev/null +++ b/src/shared/components/gh-spinner/gh-spinner-icon.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +export interface GhSpinnerIconProps { + className: string; +} + +export const GhSpinnerIcon: React.FC = (props) => { + return ( + + + + + + ); +}; diff --git a/src/shared/components/gh-spinner/gh-spinner.module.scss b/src/shared/components/gh-spinner/gh-spinner.module.scss new file mode 100644 index 0000000..54b38b7 --- /dev/null +++ b/src/shared/components/gh-spinner/gh-spinner.module.scss @@ -0,0 +1,3 @@ +.speed { + --pf-c-spinner--AnimationDuration: 0.5s !important; +} diff --git a/src/shared/components/gh-spinner/gh-spinner.tsx b/src/shared/components/gh-spinner/gh-spinner.tsx new file mode 100644 index 0000000..0fa8e79 --- /dev/null +++ b/src/shared/components/gh-spinner/gh-spinner.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { GhSpinnerIcon } from "./gh-spinner-icon"; +import styles from "./gh-spinner.module.scss"; + +export interface GhSpinnerProps { + size?: "sm" | "md" | "lg" | "xl"; +} + +export const GhSpinner: React.FC = ({ size = "md" }) => { + return ( + + ); +}; diff --git a/src/shared/components/gh-spinner/index.ts b/src/shared/components/gh-spinner/index.ts new file mode 100644 index 0000000..4cc60a2 --- /dev/null +++ b/src/shared/components/gh-spinner/index.ts @@ -0,0 +1 @@ +export { GhSpinner } from "./gh-spinner"; diff --git a/src/shared/components/gh-spinner/stories/gh-spinner.stories.tsx b/src/shared/components/gh-spinner/stories/gh-spinner.stories.tsx new file mode 100644 index 0000000..1af7104 --- /dev/null +++ b/src/shared/components/gh-spinner/stories/gh-spinner.stories.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Story, Meta } from "@storybook/react/types-6-0"; +import { GhSpinner, GhSpinnerProps } from "../gh-spinner"; + +export default { + title: "Components / GhSpinner", + component: GhSpinner, +} as Meta; + +const Template: Story = (args) => ; + +export const Md = Template.bind({}); +Md.args = { + size: "md", +}; diff --git a/src/shared/components/gh-spinner/tests/gh-spinner.test.tsx b/src/shared/components/gh-spinner/tests/gh-spinner.test.tsx new file mode 100644 index 0000000..ff92841 --- /dev/null +++ b/src/shared/components/gh-spinner/tests/gh-spinner.test.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { GhSpinner } from "../gh-spinner"; + +describe("GhSpinner", () => { + it("Renders without crashing", () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/shared/components/index.ts b/src/shared/components/index.ts index 02f04b1..fcb630f 100644 --- a/src/shared/components/index.ts +++ b/src/shared/components/index.ts @@ -5,9 +5,11 @@ export { AppTableWithControls } from "./app-table-with-controls"; export { CompanyContextSelectorSection } from "./company-context-selector-section"; export { ConditionalRender } from "./conditional-render"; export { DeleteModalWithMatch } from "./delete-modal-with-match"; +export { DocumentStatus } from "./document-status"; export { ErrorEmptyState } from "./error-empty-state"; export { SearchFilter } from "./search-filter"; export { SimpleContextSelector } from "./simple-context-selector"; export { SimplePageSection } from "./simple-page-section"; export { SimplePagination } from "./simple-pagination"; export { UploadFilesDropzone } from "./upload-files-dropzone"; +export { WsWatcher } from "./ws-watcher"; diff --git a/src/shared/components/ws-watcher/index.ts b/src/shared/components/ws-watcher/index.ts new file mode 100644 index 0000000..945b1d8 --- /dev/null +++ b/src/shared/components/ws-watcher/index.ts @@ -0,0 +1 @@ +export { WsWatcher } from "./ws-watcher"; diff --git a/src/shared/components/ws-watcher/ws-watcher.tsx b/src/shared/components/ws-watcher/ws-watcher.tsx new file mode 100644 index 0000000..e38b224 --- /dev/null +++ b/src/shared/components/ws-watcher/ws-watcher.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import useWebSocket from "react-use-websocket"; +import { useKeycloak } from "@react-keycloak/web"; + +export interface ChildrenProps { + lastMessage: MessageEvent | null; + lastJsonMessage: any; +} + +export interface ProjectStatusWatcherProps { + url: string; + children: (args: ChildrenProps) => any; +} + +export const WsWatcher: React.FC = ({ + url, + children, +}) => { + const { keycloak } = useKeycloak(); + + const { lastMessage, lastJsonMessage, sendJsonMessage } = useWebSocket(url, { + onOpen: () => { + sendJsonMessage({ + authentication: { + token: keycloak.token, + }, + }); + }, + shouldReconnect: (event: CloseEvent) => event.code !== 1011, + share: true, + }); + + return children({ + lastMessage, + lastJsonMessage, + }); +}; diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts index 8052a03..8cbea9a 100644 --- a/src/shared/hooks/index.ts +++ b/src/shared/hooks/index.ts @@ -1,3 +1,4 @@ +export { useColSelectionState } from "./useColSelectionState"; export { useDeleteCompany } from "./useDeleteCompany"; export { useFetchCompanies } from "./useFetchCompanies"; export { useFetchCompany } from "./useFetchCompany"; diff --git a/src/shared/hooks/useColSelectionState/index.ts b/src/shared/hooks/useColSelectionState/index.ts new file mode 100644 index 0000000..21a5f5b --- /dev/null +++ b/src/shared/hooks/useColSelectionState/index.ts @@ -0,0 +1 @@ +export { useColSelectionState } from "./useColSelectionState"; diff --git a/src/shared/hooks/useColSelectionState/useColSelectionState.tsx b/src/shared/hooks/useColSelectionState/useColSelectionState.tsx new file mode 100644 index 0000000..e8ea51a --- /dev/null +++ b/src/shared/hooks/useColSelectionState/useColSelectionState.tsx @@ -0,0 +1,84 @@ +import React from "react"; + +export interface ColSelection { + row: T; + colIndex: number; +} + +export interface IColSelectionStateArgs { + rows: T[]; + initialSelected?: ColSelection[]; + isEqual?: (a: T, b: T) => boolean; + externalState?: [ + ColSelection[], + React.Dispatch[]>> + ]; +} + +export interface IColSelectionState { + selectedCols: ColSelection[]; + isColSelected: (row: T, colIndex: number) => boolean; + toggleSelectedCol: (row: T, colIndex: number, isSelecting?: boolean) => void; + toggleSelectedColSingle: ( + rows: T, + colIndex: number, + isSelecting?: boolean + ) => void; + setSelectedCols: (cols: ColSelection[]) => void; +} + +export const useColSelectionState = ({ + rows, + initialSelected = [], + isEqual = (a, b) => a === b, + externalState, +}: IColSelectionStateArgs): IColSelectionState => { + const internalState = React.useState[]>(initialSelected); + const [selectedCols, setSelectedCols] = externalState || internalState; + + const isColSelected = (row: T, colIndex: number) => { + return selectedCols.some( + (i) => isEqual(i.row, row) && i.colIndex === colIndex + ); + }; + + const toggleColSelected = ( + row: T, + colIndex: number, + isSelecting = !isColSelected(row, colIndex) + ) => { + if (isSelecting) { + setSelectedCols([...selectedCols, { colIndex, row }]); + } else { + setSelectedCols( + selectedCols.filter( + (i) => !(isEqual(i.row, row) && i.colIndex === colIndex) + ) + ); + } + }; + + const toggleSelectedColSingle = ( + row: T, + colIndex: number, + isSelecting = !isColSelected(row, colIndex) + ) => { + const otherSelectedCols = selectedCols.filter( + (selected) => !isEqual(selected.row, row) + ); + + if (isSelecting) { + setSelectedCols([...otherSelectedCols, { row, colIndex }]); + } else { + setSelectedCols(otherSelectedCols); + } + }; + + return { + selectedCols, + isColSelected: isColSelected, + toggleSelectedCol: toggleColSelected, + toggleSelectedColSingle: toggleSelectedColSingle, + setSelectedCols: setSelectedCols, + }; +}; diff --git a/src/shared/hooks/useFetchDocuments/useFetchDocuments.ts b/src/shared/hooks/useFetchDocuments/useFetchDocuments.ts index 715f565..856af2e 100644 --- a/src/shared/hooks/useFetchDocuments/useFetchDocuments.ts +++ b/src/shared/hooks/useFetchDocuments/useFetchDocuments.ts @@ -2,6 +2,8 @@ import { useCallback, useReducer } from "react"; import { AxiosError } from "axios"; import { ActionType, createAsyncAction, getType } from "typesafe-actions"; +import debounce from "lodash/debounce"; + import { UBLDocumentSortByQuery, getDocuments } from "api/rest"; import { PageRepresentation, UBLDocument, PageQuery } from "api/models"; @@ -67,6 +69,10 @@ const reducer = (state: State, action: Action): State => { } }; +// + +// + export interface IState { documents?: PageRepresentation; isFetching: boolean; @@ -80,6 +86,14 @@ export interface IState { page: PageQuery, sortBy?: UBLDocumentSortByQuery ) => void; + fetchDocumentsStream: ( + company: string, + filters: { + filterText?: string; + }, + page: PageQuery, + sortBy?: UBLDocumentSortByQuery + ) => void; } export const useFetchDocuments = ( @@ -109,12 +123,97 @@ export const useFetchDocuments = ( [] ); + // const fetchDocumentsWithAutoRefresh = useCallback( + // ( + // company: string, + // filters: { + // filterText?: string; + // }, + // page: PageQuery, + // sortBy?: UBLDocumentSortByQuery + // ) => { + // dispatch(fetchRequest()); + + // getDocuments(company, filters, page, sortBy) + // .then(({ data }) => { + // dispatch(fetchSuccess(data)); + + // const shouldReload = data.data.some((f) => { + // return ( + // f.sunatDeliveryStatus === "SCHEDULED_TO_DELIVER" || + // f.sunatDeliveryStatus === "NEED_TO_CHECK_TICKET" + // ); + // }); + + // if (shouldReload) { + // setTimeout(() => { + // debouncedFetchDocuments(company, filters, page, sortBy); + // }, 1000); + // } + // }) + // .catch((error: AxiosError) => { + // dispatch(fetchFailure(error)); + // }); + // }, + // [] + // ); + + const debouncedFetchDocuments = useCallback( + debounce( + ( + company: string, + filters: { + filterText?: string; + }, + page: PageQuery, + sortBy?: UBLDocumentSortByQuery + ) => { + const fetchFn = ( + company: string, + filters: { + filterText?: string; + }, + page: PageQuery, + sortBy?: UBLDocumentSortByQuery + ) => { + dispatch(fetchRequest()); + + getDocuments(company, filters, page, sortBy) + .then(({ data }) => { + dispatch(fetchSuccess(data)); + + const shouldReload = data.data.some((f) => { + return ( + f.sunatDeliveryStatus === "SCHEDULED_TO_DELIVER" || + f.sunatDeliveryStatus === "NEED_TO_CHECK_TICKET" + ); + }); + + if (shouldReload) { + setTimeout(() => { + debouncedFetchDocuments(company, filters, page, sortBy); + }, 1000); + } + }) + .catch((error: AxiosError) => { + dispatch(fetchFailure(error)); + }); + }; + + return fetchFn(company, filters, page, sortBy); + }, + 500 + ), + [] + ); + return { documents: state.documents, isFetching: state.isFetching, fetchError: state.fetchError, fetchCount: state.fetchCount, fetchDocuments, + fetchDocumentsStream: debouncedFetchDocuments, }; }; diff --git a/yarn.lock b/yarn.lock index 31777cb..e9e84b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1786,6 +1786,14 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@konveyor/lib-ui@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@konveyor/lib-ui/-/lib-ui-2.0.0.tgz#9b23de100095d1195ecc53c4c2d5c9fa959afdee" + integrity sha512-x10WhEz34wOiRqNZTfNfsOfkFgv5GkYBI1J1hVmEHNZgEZrKiz1axO0fxQAP1xpes5cU/nLvhTL+oGDJnwFXnA== + dependencies: + fast-deep-equal "^3.1.3" + yup "^0.29.3" + "@mdx-js/loader@^1.5.1": version "1.6.19" resolved "https://registry.yarnpkg.com/@mdx-js/loader/-/loader-1.6.19.tgz#2b90dee54b6f959539f310776f18fe24e1f15cc5" @@ -1871,56 +1879,50 @@ dependencies: mkdirp "^1.0.4" -"@patternfly/patternfly@4.87.0": - version "4.87.0" - resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-4.87.0.tgz#072dffb7988d88056d055adb8dd3875b79ef6ca4" - integrity sha512-1mQR1iUVG/C8MjuNoQV4iSxIZx+duTakqYoV83MqBW5lvHou5TMM4gQtIi4CszSBVFnwQGnkpnapPg4+IlLbNw== +"@patternfly/patternfly@4.90.5": + version "4.90.5" + resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-4.90.5.tgz#49aca1c49837e5bf877cdb9ffe454d97bf48e587" + integrity sha512-Fe0C8UkzSjtacQ+fHXlFB/LHzrv/c2K4z479C6dboOgkGQE1FyB0wt1NBfxij0D++rhOy04OOYdE+Tr0JSlZKw== -"@patternfly/patternfly@4.87.1": - version "4.87.1" - resolved "https://registry.yarnpkg.com/@patternfly/patternfly/-/patternfly-4.87.1.tgz#321ec6ad3899a27268de2bbe52f7dcd293a80e2e" - integrity sha512-Q9injbH8Qk443CsmCeeXHlDpLDwB/+ak4kIjk1wrpqvgtXK844fcsX4x95upipiqjwlTocNfIvE5IbI1o/JV0Q== - -"@patternfly/react-core@4.97.0", "@patternfly/react-core@^4.97.0": - version "4.97.0" - resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-4.97.0.tgz#b5e77a32cefcfef046eddae406b3b93a901ff36d" - integrity sha512-HNjk9wJtLRXYy3faodNqFsL4xWqpu+imnoTeTq+2B3kNLiRW1GNgZ2MQtDZ96zMloAeqYPGfeHKCk7wfrhV7kw== +"@patternfly/react-core@4.101.2", "@patternfly/react-core@^4.101.2": + version "4.101.2" + resolved "https://registry.yarnpkg.com/@patternfly/react-core/-/react-core-4.101.2.tgz#a4a3f06513387f5ce5d7b5d36ceea11270e36267" + integrity sha512-8z+f6AlFcrZUn5vyOZtbHjY+hiI/9sqLVLnEonBLaJ7jx2/x8MS+G7H7VL1uTYsf06dy3MVdEKWi7TgziHc8gQ== dependencies: - "@patternfly/react-icons" "^4.9.0" - "@patternfly/react-styles" "^4.8.0" - "@patternfly/react-tokens" "^4.10.0" + "@patternfly/react-icons" "^4.9.4" + "@patternfly/react-styles" "^4.8.4" + "@patternfly/react-tokens" "^4.10.4" focus-trap "6.2.2" react-dropzone "9.0.0" tippy.js "5.1.2" tslib "1.13.0" -"@patternfly/react-icons@^4.9.0": - version "4.9.0" - resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-4.9.0.tgz#6169facdce016b81eabd2384445558ec2a2f4fed" - integrity sha512-riJQnf+V5clbPSqD0bqMeMCziwSdIMot8GAigG62eXaT+w5teBisk29t3Dc1DuWq7n6s5WWNzOZ5S42cmU2Edw== - -"@patternfly/react-styles@^4.8.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-4.8.0.tgz#1f496f8ecf68dd75ce7713018efeb94a4c99d8c4" - integrity sha512-++odmYozQPJv9UbZyJU/sgb7uAHhHYLSS8rP5zNVoqFQJN9oYwgDOrWFL2m5gY4x0k3qa0XI2WXhBLoaxxX6sQ== - -"@patternfly/react-table@4.23.0": - version "4.23.0" - resolved "https://registry.yarnpkg.com/@patternfly/react-table/-/react-table-4.23.0.tgz#f1d66a23c2bc3bd68cf3259ee08cbbe8c98a5b7c" - integrity sha512-0vX9BFiAcGcZu60+ZXkppuNov74lOFSKGEvZbVbpTaH6mnKReWxlkFQQYEb7eWCPrBaYkVDDz6O3XEZnqJifwg== - dependencies: - "@patternfly/patternfly" "4.87.0" - "@patternfly/react-core" "^4.97.0" - "@patternfly/react-icons" "^4.9.0" - "@patternfly/react-styles" "^4.8.0" - "@patternfly/react-tokens" "^4.10.0" +"@patternfly/react-icons@^4.9.4": + version "4.9.4" + resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-4.9.4.tgz#3537a17c224b3b5306fb73b46a612a1b8a59d7bc" + integrity sha512-ZsK6AzaX1RL0HbzS08AOJOfLjxn1D6DqAkoZh/bXPXGuH1X75gfF3Ynuuo1QlGMG2m8YeOVCO5KLR4Fc9rcShg== + +"@patternfly/react-styles@^4.8.4": + version "4.8.4" + resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-4.8.4.tgz#62a50708399fe6a8586074ccfb331559122648f0" + integrity sha512-0k/d/OuW55nWt0pEFvsHX+9qyrWLusLsAWnsaqb4X+4aab7+RS296wMdy60RWfL78+DLIeIis09OXLHYkypqRg== + +"@patternfly/react-table@4.23.13": + version "4.23.13" + resolved "https://registry.yarnpkg.com/@patternfly/react-table/-/react-table-4.23.13.tgz#1b878d7c2dee8d225356172098a585db9912cb5e" + integrity sha512-qj2Jazrv2+i2M3FPBzFrTmQEyTQY6rm2JVnui4uon2etTNDjPAM89FDHNKX0JrT3euOFLibks/pGSHAtYhOGVQ== + dependencies: + "@patternfly/react-core" "^4.101.2" + "@patternfly/react-icons" "^4.9.4" + "@patternfly/react-styles" "^4.8.4" + "@patternfly/react-tokens" "^4.10.4" lodash "^4.17.19" tslib "1.13.0" -"@patternfly/react-tokens@^4.10.0": - version "4.10.0" - resolved "https://registry.yarnpkg.com/@patternfly/react-tokens/-/react-tokens-4.10.0.tgz#251fe04416d78e4fd124f2e0c5e1f3b71a320edf" - integrity sha512-jweUTx/QV6sDmYySrsMrMVhmPlQwSv5bn/BPnjpNZJuny9wBMoG3rsfx+UYaliIwYGrQColxSVW+aYmJgkJezw== +"@patternfly/react-tokens@^4.10.4": + version "4.10.4" + resolved "https://registry.yarnpkg.com/@patternfly/react-tokens/-/react-tokens-4.10.4.tgz#4abee2674610ce96ed5b5857861f24021b8c4a6f" + integrity sha512-20MLDNENqFP0ZsJ2nhXDts1WU5c1N8UAQRndui1PkOUqed2YkUlREbrGQPxCatq3QJ75Dq17ezQYuWplZqC2lg== "@pmmmwh/react-refresh-webpack-plugin@0.4.2": version "0.4.2" @@ -2984,6 +2986,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/lodash@^4.14.168": + version "4.14.168" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" + integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q== + "@types/markdown-to-jsx@^6.11.0": version "6.11.3" resolved "https://registry.yarnpkg.com/@types/markdown-to-jsx/-/markdown-to-jsx-6.11.3.tgz#cdd1619308fecbc8be7e6a26f3751260249b020e" @@ -11230,6 +11237,11 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" @@ -11786,7 +11798,7 @@ module-deps@^6.0.0, module-deps@^6.2.3: through2 "^2.0.0" xtend "^4.0.0" -moment@^2.27.0: +moment@^2.27.0, moment@^2.29.1: version "2.29.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -14168,6 +14180,11 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-moment@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-1.1.1.tgz#5fe9fb257039590c804e2b3aedfc3ceb0a6ffb16" + integrity sha512-WjwvxBSnmLMRcU33do0KixDB+9vP3e84eCse+rd+HNklAMNWyRgZTDEQlay/qK6lcXFPRuEIASJTpEt6pyK7Ww== + react-popper-tooltip@^2.11.0: version "2.11.1" resolved "https://registry.yarnpkg.com/react-popper-tooltip/-/react-popper-tooltip-2.11.1.tgz#3c4bdfd8bc10d1c2b9a162e859bab8958f5b2644" @@ -14339,6 +14356,11 @@ react-textarea-autosize@^8.1.1: use-composed-ref "^1.0.0" use-latest "^1.0.0" +react-use-websocket@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/react-use-websocket/-/react-use-websocket-2.5.0.tgz#20c6657802369a280c4921840a0529c2fd0cfe6f" + integrity sha512-xC2xE13RI584dyRtuQaTRf9tyJsdL4wD2fqq8ugZAcm3Xf+qjyvTJiB2BMsWfBOjQgnjOJstp2sOPXFB84Sb+Q== + react@^16.13.1, react@^16.8.3: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"