diff --git a/frontend/src/component/application/ApplicationEdit/ApplicationEdit.tsx b/frontend/src/component/application/ApplicationEdit/ApplicationEdit.tsx index cab7262337..aea2b3924e 100644 --- a/frontend/src/component/application/ApplicationEdit/ApplicationEdit.tsx +++ b/frontend/src/component/application/ApplicationEdit/ApplicationEdit.tsx @@ -16,6 +16,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import { UPDATE_APPLICATION } from 'component/providers/AccessProvider/permissions'; import { ApplicationView } from '../ApplicationView/ApplicationView'; import { ApplicationUpdate } from '../ApplicationUpdate/ApplicationUpdate'; +import { ConnectedInstances } from '../ConnectedInstances/ConnectedInstances'; import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; @@ -30,8 +31,10 @@ import { formatDateYMD } from 'utils/formatDate'; import { formatUnknownError } from 'utils/formatUnknownError'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel'; +import { useUiFlag } from 'hooks/useUiFlag'; export const ApplicationEdit = () => { + const showAdvancedApplicationMetrics = useUiFlag('sdkReporting'); const navigate = useNavigate(); const name = useRequiredPathParam('name'); const { application, loading } = useApplication(name); @@ -84,6 +87,13 @@ export const ApplicationEdit = () => { }, ]; + if (showAdvancedApplicationMetrics) { + tabData.push({ + label: 'Connected instances', + component: , + }); + } + if (loading) { return (
diff --git a/frontend/src/component/application/ConnectedInstances/ConnectedInstances.tsx b/frontend/src/component/application/ConnectedInstances/ConnectedInstances.tsx new file mode 100644 index 0000000000..22bb1718e2 --- /dev/null +++ b/frontend/src/component/application/ConnectedInstances/ConnectedInstances.tsx @@ -0,0 +1,53 @@ +import { useMemo } from 'react'; +import useApplication from 'hooks/api/getters/useApplication/useApplication'; +import { formatDateYMDHMS } from 'utils/formatDate'; +import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import { useConnectedInstancesTable } from './useConnectedInstancesTable'; +import { ConnectedInstancesTable } from './ConnectedInstancesTable'; + +export const ConnectedInstances = () => { + const name = useRequiredPathParam('name'); + const { application } = useApplication(name); + + const tableData = useMemo(() => { + return ( + application.instances + // @ts-expect-error: the type definition here is incomplete. It + // should be updated as part of this project. + .filter((instance) => instance.environment === 'production') + .map(({ instanceId, sdkVersion, clientIp, lastSeen }) => { + return { + instanceId, + ip: clientIp, + sdkVersion, + lastSeen: formatDateYMDHMS(lastSeen), + }; + }) + ); + }, [application]); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + state: { globalFilter }, + setHiddenColumns, + columns, + } = useConnectedInstancesTable(tableData); + + return ( + + ); +}; diff --git a/frontend/src/component/application/ConnectedInstances/ConnectedInstancesTable.tsx b/frontend/src/component/application/ConnectedInstances/ConnectedInstancesTable.tsx new file mode 100644 index 0000000000..0e0a995967 --- /dev/null +++ b/frontend/src/component/application/ConnectedInstances/ConnectedInstancesTable.tsx @@ -0,0 +1,109 @@ +import { + Row, + TablePropGetter, + TableProps, + TableBodyPropGetter, + TableBodyProps, + HeaderGroup, +} from 'react-table'; +import { + SortableTableHeader, + TableCell, + TablePlaceholder, +} from 'component/common/Table'; +import { + Box, + styled, + Table, + TableBody, + TableRow, + useMediaQuery, +} from '@mui/material'; +import theme from 'themes/theme'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; + +import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; + +const hiddenColumnsSmall = ['ip', 'sdkVersion']; +const hiddenColumnsCompact = ['ip', 'sdkVersion', 'lastSeen']; + +type ConnectedInstancesTableProps = { + compact?: boolean; + loading: boolean; + setHiddenColumns: (param: any) => void; + columns: any[]; + rows: Row[]; + prepareRow: (row: Row) => void; + getTableProps: ( + propGetter?: TablePropGetter | undefined, + ) => TableProps; + getTableBodyProps: ( + propGetter?: TableBodyPropGetter | undefined, + ) => TableBodyProps; + headerGroups: HeaderGroup[]; + globalFilter: any; +}; +export const ConnectedInstancesTable = ({ + compact = false, + setHiddenColumns, + columns, + loading, + rows, + getTableProps, + getTableBodyProps, + headerGroups, + globalFilter, + prepareRow, +}: ConnectedInstancesTableProps) => { + const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')); + + useConditionallyHiddenColumns( + [ + { + condition: isSmallScreen, + columns: hiddenColumnsSmall, + }, + { + condition: compact, + columns: hiddenColumnsCompact, + }, + ], + setHiddenColumns, + columns, + ); + + return ( + <> + + + + + {rows.map((row) => { + prepareRow(row); + return ( + + {row.cells.map((cell) => ( + + {cell.render('Cell')} + + ))} + + ); + })} + +
+
+ +

+ There's no data for any connected instances to + display. Have you configured your clients correctly? +

+ + } + /> + + ); +}; diff --git a/frontend/src/component/application/ConnectedInstances/useConnectedInstancesTable.tsx b/frontend/src/component/application/ConnectedInstances/useConnectedInstancesTable.tsx new file mode 100644 index 0000000000..e00dc6730e --- /dev/null +++ b/frontend/src/component/application/ConnectedInstances/useConnectedInstancesTable.tsx @@ -0,0 +1,97 @@ +import { useMemo } from 'react'; +import { IApiToken } from 'hooks/api/getters/useApiTokens/useApiTokens'; +import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; +import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; +import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; +import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; +import { useTable, useGlobalFilter, useSortBy } from 'react-table'; +import { sortTypes } from 'utils/sortTypes'; +import { ProjectsList } from 'component/admin/apiToken/ProjectsList/ProjectsList'; +import { Key } from '@mui/icons-material'; + +type ConnectedInstancesTableData = { + instanceId: string; + ip: string; + sdkVersion: string; + lastSeen: string; +}; + +export const useConnectedInstancesTable = ( + instanceData: ConnectedInstancesTableData[], +) => { + const initialState = useMemo( + () => ({ sortBy: [{ id: 'instanceId' }] }), + [], + ); + + const COLUMNS = useMemo(() => { + return [ + { + Header: 'Instances', + accessor: 'instanceId', + Cell: HighlightCell, + styles: { + width: '45%', + }, + }, + { + Header: 'SDK', + accessor: 'sdkVersion', + Cell: HighlightCell, + styles: { + width: '20%', + }, + }, + { + Header: 'Last seen', + accessor: 'lastSeen', + Cell: HighlightCell, + styles: { + width: '20%', + }, + }, + { + Header: 'IP', + accessor: 'ip', + Cell: HighlightCell, + styles: { + width: '15%', + }, + }, + ]; + }, []); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + state, + setGlobalFilter, + setHiddenColumns, + } = useTable( + { + columns: COLUMNS as any, + data: instanceData as any, + initialState, + sortTypes, + autoResetHiddenColumns: false, + disableSortRemove: true, + }, + useGlobalFilter, + useSortBy, + ); + + return { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + state, + setGlobalFilter, + setHiddenColumns, + columns: COLUMNS, + }; +};