Skip to content

Commit

Permalink
[feat] Add history timeline (#289)
Browse files Browse the repository at this point in the history
  • Loading branch information
sijav authored Aug 9, 2024
1 parent 1fede17 commit bcdf19b
Show file tree
Hide file tree
Showing 15 changed files with 479 additions and 28 deletions.
19 changes: 14 additions & 5 deletions src/locales/de-DE/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ msgstr ""
msgid "Compute changes in the last 30 days."
msgstr ""

#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:28
#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:17
#: src/pages/panel/resource-detail/utils/nodeChange.tsx:32
msgid "Configuration changed"
msgstr "Konfiguration geändert"
Expand Down Expand Up @@ -1838,10 +1838,19 @@ msgstr "Verwaiste Snapshots"
msgid "Orphaned Volumes"
msgstr "Verwaiste Volumes"

#: src/pages/panel/inventory/InventoryInfoResourcesPerAccountTimeline.tsx:35
msgid "Other {0} accounts"
msgstr ""

#: src/pages/panel/workspace-settings/WorkspaceSettingsPage.tsx:45
msgid "Other Workspace Settings"
msgstr "Andere Arbeitsbereichseinstellungen"

#: src/pages/panel/inventory/InventoryInfoResourcesPerAccountTimeline.tsx:137
#: src/pages/panel/inventory/InventoryInfoResourcesPerAccountTimeline.tsx:160
msgid "Others"
msgstr ""

#: src/pages/auth/login/LoginPage.tsx:140
#: src/pages/panel/user-settings/UserSettingsTotpActivationModal.tsx:215
#: src/pages/panel/user-settings/UserSettingsTotpDeactivationModal.tsx:73
Expand Down Expand Up @@ -2084,17 +2093,17 @@ msgstr ""
msgid "Resource changes with owner tag"
msgstr ""

#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:18
#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:15
#: src/pages/panel/resource-detail/utils/nodeChange.tsx:30
msgid "Resource created"
msgstr "Ressource erstellt"

#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:38
#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:19
#: src/pages/panel/resource-detail/utils/nodeChange.tsx:34
msgid "Resource deleted"
msgstr "Ressource gelöscht"

#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:48
#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:23
msgid "Resource Secured"
msgstr ""

Expand Down Expand Up @@ -2196,7 +2205,7 @@ msgstr ""
msgid "Security Issues"
msgstr "Sicherheitsprobleme"

#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:58
#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:21
#: src/pages/panel/resource-detail/utils/nodeChange.tsx:41
msgid "Security posture changed"
msgstr ""
Expand Down
19 changes: 14 additions & 5 deletions src/locales/en-US/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ msgstr "Compute Changes"
msgid "Compute changes in the last 30 days."
msgstr "Compute changes in the last 30 days."

#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:28
#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:17
#: src/pages/panel/resource-detail/utils/nodeChange.tsx:32
msgid "Configuration changed"
msgstr "Configuration changed"
Expand Down Expand Up @@ -1838,10 +1838,19 @@ msgstr "Orphaned Snapshots"
msgid "Orphaned Volumes"
msgstr "Orphaned Volumes"

#: src/pages/panel/inventory/InventoryInfoResourcesPerAccountTimeline.tsx:35
msgid "Other {0} accounts"
msgstr "Other {0} accounts"

#: src/pages/panel/workspace-settings/WorkspaceSettingsPage.tsx:45
msgid "Other Workspace Settings"
msgstr "Other Workspace Settings"

#: src/pages/panel/inventory/InventoryInfoResourcesPerAccountTimeline.tsx:137
#: src/pages/panel/inventory/InventoryInfoResourcesPerAccountTimeline.tsx:160
msgid "Others"
msgstr "Others"

#: src/pages/auth/login/LoginPage.tsx:140
#: src/pages/panel/user-settings/UserSettingsTotpActivationModal.tsx:215
#: src/pages/panel/user-settings/UserSettingsTotpDeactivationModal.tsx:73
Expand Down Expand Up @@ -2084,17 +2093,17 @@ msgstr "Resource Changes"
msgid "Resource changes with owner tag"
msgstr "Resource changes with owner tag"

#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:18
#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:15
#: src/pages/panel/resource-detail/utils/nodeChange.tsx:30
msgid "Resource created"
msgstr "Resource created"

#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:38
#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:19
#: src/pages/panel/resource-detail/utils/nodeChange.tsx:34
msgid "Resource deleted"
msgstr "Resource deleted"

#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:48
#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:23
msgid "Resource Secured"
msgstr "Resource Secured"

Expand Down Expand Up @@ -2196,7 +2205,7 @@ msgstr "Security Changes"
msgid "Security Issues"
msgstr "Security Issues"

#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:58
#: src/pages/panel/inventory-search/utils/inventoryRenderNodeChangeCell.tsx:21
#: src/pages/panel/resource-detail/utils/nodeChange.tsx:41
msgid "Security posture changed"
msgstr "Security posture changed"
Expand Down
215 changes: 215 additions & 0 deletions src/pages/panel/inventory-search/InventoryChangesTimeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { useLingui } from '@lingui/react'
import { Box, colors } from '@mui/material'
import { BarChart, BarChartProps } from '@mui/x-charts'
import { useQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { useMemo } from 'react'
import { useUserProfile } from 'src/core/auth'
import { useThemeMode } from 'src/core/theme'
import { useFixQueryParser } from 'src/shared/fix-query-parser'
import { LoadingSuspenseFallback } from 'src/shared/loading'
import { WorkspaceInventoryNodeHistoryChanges } from 'src/shared/types/server'
import { getNumberFormatter } from 'src/shared/utils/getNumberFormatter'
import { durationToCustomDurationString } from 'src/shared/utils/parseDuration'
import { postWorkspaceInventoryHistoryTimelineQuery } from './postWorkspaceInventoryHistoryTimeline.query'
import { inventoryRenderNodeChangeCellToString } from './utils/inventoryRenderNodeChangeCell'

interface InventoryChangesTimelineProps {
searchCrit: string
}

const getColorFromHistoryChange = (change: WorkspaceInventoryNodeHistoryChanges, isDark?: boolean) => {
switch (change) {
case 'node_created':
return colors.blue[isDark ? '700' : '400']
case 'node_updated':
return colors.blueGrey[isDark ? '700' : '400']
case 'node_deleted':
return colors.deepOrange[isDark ? '700' : '400']
case 'node_vulnerable':
return colors.red[isDark ? '700' : '400']
case 'node_compliant':
return colors.green[isDark ? '700' : '400']
}
}

const DEFAULT_LABELS_LENGTH = 26

export const InventoryChangesTimeline = ({ searchCrit }: InventoryChangesTimelineProps) => {
const {
history: { changes, after, before },
onHistoryChange,
} = useFixQueryParser()
const beforeDate = useMemo(() => {
const now = new Date()
now.setMilliseconds(0)
if (!before) {
return now
}
const beforeDate = new Date(before)
beforeDate.setMilliseconds(0)
if (beforeDate.valueOf() > now.valueOf()) {
return now
}
return beforeDate
}, [before])
const afterDate = useMemo(() => {
if (!after) {
const now = new Date()
now.setMilliseconds(0)
const aDayBefore = new Date(now.valueOf())
aDayBefore.setMonth(now.getMonth() - 1)
return aDayBefore
} else {
const afterDate = new Date(after)
afterDate.setMilliseconds(0)
return afterDate
}
}, [after])
const { selectedWorkspace } = useUserProfile()
const { mode } = useThemeMode()

const { labelsDur, granularity } = useMemo(() => {
const granularity =
Math.max(Math.floor(Math.abs((beforeDate.valueOf() / 1000 - afterDate.valueOf() / 1000) / DEFAULT_LABELS_LENGTH)), 60 * 60) * 1000
const labelsDur = []
const end = beforeDate.valueOf()
for (let current = afterDate.valueOf(); current < end; current += granularity) {
labelsDur.push(current)
}
return { labelsDur, granularity }
}, [afterDate, beforeDate])

const isGranularityMoreThanADay = granularity >= 24 * 60 * 60 * 1000

const {
i18n: { locale },
} = useLingui()
const { data, isLoading } = useQuery({
queryKey: [
'workspace-inventory-history-timeline',
selectedWorkspace?.id,
searchCrit || 'all',
changes.sort().join(','),
afterDate.toISOString(),
beforeDate.toISOString(),
durationToCustomDurationString({ duration: granularity }),
],
queryFn: postWorkspaceInventoryHistoryTimelineQuery,
})

const dummySeries = useMemo(() => {
const numberFormatter = getNumberFormatter(locale)
return changes.reduce(
(prev, change) => ({
...prev,
[change]: {
valueFormatter: numberFormatter,
data: labelsDur.map(() => 0),
label: inventoryRenderNodeChangeCellToString(change),
stack: 'total',
color: getColorFromHistoryChange(change, mode === 'dark'),
stackOffset: 'none',
},
}),
{} as Record<WorkspaceInventoryNodeHistoryChanges, BarChartProps['series'][number]>,
)
}, [changes, labelsDur, locale, mode])

const [series, labels] = useMemo(() => {
const labels = labelsDur.map((item) => new Date(item))
if (data) {
const [series, seriesChanges] = (Object.keys(dummySeries) as WorkspaceInventoryNodeHistoryChanges[]).reduce(
(prev, change) => [
[...prev[0], dummySeries[change]],
[...prev[1], change],
],
[[], []] as [BarChartProps['series'], WorkspaceInventoryNodeHistoryChanges[]],
)
data.forEach(({ at, group: { change }, v }) => {
const foundSeriesIndex = seriesChanges.indexOf(change)
const foundDataIndex = labelsDur.indexOf(new Date(at).valueOf())
if (foundDataIndex > -1 && foundSeriesIndex > -1 && series[foundSeriesIndex] && series[foundSeriesIndex].data) {
series[foundSeriesIndex].data = [...series[foundSeriesIndex].data]
series[foundSeriesIndex].data[foundDataIndex] = v
}
})
return [series, labels]
}
return [[] as BarChartProps['series'], labels]
}, [data, dummySeries, labelsDur])

return !isLoading && !data ? null : (
<Box width="100%" overflow="auto">
<Box width="100%" maxWidth={!labels.length ? '100%' : labels.length * 62 + 150} minWidth={labels.length * 20 + 150} height={500}>
{isLoading ? (
<LoadingSuspenseFallback />
) : (
<BarChart
slotProps={{
legend: {
direction: 'row',
position: {
vertical: 'top',
horizontal: labels.length < 6 ? 'right' : 'middle',
},
itemMarkWidth: 10,
itemMarkHeight: 5,
labelStyle: {
fontSize: 12,
fontWeight: 'bold',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
itemGap: 5,
markGap: 5,
padding: 5,
},
}}
margin={{ top: 50 }}
borderRadius={4}
series={series}
yAxis={[{ scaleType: 'sqrt' }]}
xAxis={[
{
scaleType: 'band',
data: labels,
valueFormatter: (val: Date, ctx) => {
if (ctx.location === 'tick') {
return dayjs(val)
.locale(locale)
.format(isGranularityMoreThanADay ? 'L' : 'l LT')
} else {
const after = dayjs(val.valueOf())
const before = dayjs(val.valueOf() + granularity)
return `${after.format(isGranularityMoreThanADay ? 'dddd, LL' : 'llll')} - ${before.format(isGranularityMoreThanADay ? 'dddd, LL' : 'llll')}`
}
},
// @ts-expect-error something
categoryGapRatio: 0.5,
},
]}
onAxisClick={
labels.length === 1
? undefined
: (_, axisData) => {
if (axisData && axisData.axisValue && typeof axisData.axisValue === 'object') {
const afterValue = axisData.axisValue.valueOf()
const beforeValue = axisData.axisValue.valueOf() + granularity
const after = new Date(afterValue)
const before = new Date(beforeValue)
onHistoryChange({
changes,
after: after.toISOString(),
before: before.toISOString(),
})
}
}
}
onItemClick={labels.length === 1 ? undefined : () => {}}
/>
)}
</Box>
</Box>
)
}
4 changes: 3 additions & 1 deletion src/pages/panel/inventory-search/InventorySearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { FixQueryProvider } from 'src/shared/fix-query-parser'
import { LoadingSuspenseFallback } from 'src/shared/loading'
import { WorkspaceInventorySearchTableHistory, WorkspaceInventorySearchTableHistoryChanges } from 'src/shared/types/server'
import { getLocationSearchValues, mergeLocationSearchValues } from 'src/shared/utils/windowLocationSearch'
import { allHistoryChangesOptions } from './inventory-form/utils/allHistoryChangesOptions'
import { InventoryAdvanceSearch } from './InventoryAdvanceSearch'
import { InventoryChangesTimeline } from './InventoryChangesTimeline'
import { InventoryTable } from './InventoryTable'
import { InventoryTableError } from './InventoryTable.error'
import { InventoryTemplateBoxes } from './InventoryTemplateBoxes'
import { allHistoryChangesOptions } from './inventory-form/utils/allHistoryChangesOptions'

interface InventorySearchPageProps {
withHistory?: boolean
Expand Down Expand Up @@ -87,6 +88,7 @@ export default function InventorySearchPage({ withHistory }: InventorySearchPage
<NetworkErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<Outlet />
</NetworkErrorBoundary>
{history.changes.length ? <InventoryChangesTimeline searchCrit={searchCrit} /> : undefined}
{(!withHistory && searchCrit) || (withHistory && history.changes.length) || hasError ? (
<>
<NetworkErrorBoundary
Expand Down
Loading

0 comments on commit bcdf19b

Please sign in to comment.