diff --git a/.changeset/funny-jobs-sit.md b/.changeset/funny-jobs-sit.md new file mode 100644 index 000000000..4e8a63810 --- /dev/null +++ b/.changeset/funny-jobs-sit.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +The slab migration failure alert was updated to match the new data format. diff --git a/.changeset/rotten-keys-smile.md b/.changeset/rotten-keys-smile.md new file mode 100644 index 000000000..561623bf5 --- /dev/null +++ b/.changeset/rotten-keys-smile.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +The slab migration failure alert was updated to include a file health indicator. diff --git a/.changeset/short-spoons-march.md b/.changeset/short-spoons-march.md new file mode 100644 index 000000000..5020d6b03 --- /dev/null +++ b/.changeset/short-spoons-march.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +The set change alert was replaced with a churn alert that features a similar breakdown of contract changes with details such as reason and size. diff --git a/.changeset/sweet-deers-end.md b/.changeset/sweet-deers-end.md new file mode 100644 index 000000000..372bf6b0b --- /dev/null +++ b/.changeset/sweet-deers-end.md @@ -0,0 +1,7 @@ +--- +'@siafoundation/renterd-js': minor +'@siafoundation/renterd-react': minor +'@siafoundation/renterd-types': minor +--- + +The alerts data field is now full typed. diff --git a/apps/renterd-e2e/src/fixtures/navigate.ts b/apps/renterd-e2e/src/fixtures/navigate.ts index 8d810bdae..e5e08a8c8 100644 --- a/apps/renterd-e2e/src/fixtures/navigate.ts +++ b/apps/renterd-e2e/src/fixtures/navigate.ts @@ -9,6 +9,14 @@ export const navigateToBuckets = step( } ) +export const navigateToAlerts = step( + 'navigate to alerts', + async ({ page }: { page: Page }) => { + await page.getByTestId('sidenav').getByLabel('Alerts').click() + await expect(page.getByTestId('navbar').getByText('Alerts')).toBeVisible() + } +) + export const navigateToContracts = step( 'navigate to contracts', async ({ page }: { page: Page }) => { diff --git a/apps/renterd-e2e/src/specs/alerts.spec.ts b/apps/renterd-e2e/src/specs/alerts.spec.ts new file mode 100644 index 000000000..5ae3a76b8 --- /dev/null +++ b/apps/renterd-e2e/src/specs/alerts.spec.ts @@ -0,0 +1,246 @@ +import { Page, test, expect } from '@playwright/test' +import { navigateToAlerts } from '../fixtures/navigate' +import { afterTest, beforeTest } from '../fixtures/beforeTest' +import { AlertsResponse, busAlertsRoute } from '@siafoundation/renterd-types' + +test.beforeEach(async ({ page }) => { + await mockApiBusAlerts(page) + await beforeTest(page) +}) + +test.afterEach(async () => { + await afterTest() +}) + +test('alert data', async ({ page }) => { + await navigateToAlerts({ page }) + + // Churn alert + const churnData = page.getByTestId('churn') + await expect(churnData.getByText('churn: 99.90%')).toBeVisible() + const churnDataContractB6 = churnData.getByTestId( + 'b6f32dc39998bd85d730d39666360225af12fbad3bc18de4df50ce09073c9393' + ) + await expect( + churnDataContractB6.getByText('host is price gouging') + ).toHaveCount(2) + await expect(churnDataContractB6.getByText('11/28/2023')).toBeVisible() + await expect(churnDataContractB6.getByText('11/27/2023')).toBeVisible() + await expect(churnDataContractB6.getByText('11/26/2023')).toBeVisible() + await expect(churnDataContractB6.getByText('30.00 MB')).toBeVisible() + await expect(churnDataContractB6.getByText('30.00 KB')).toBeVisible() + await expect(churnDataContractB6.getByText('30 B')).toBeVisible() + + // Slab migration failed alert + const objectsData = page.getByTestId('objects') + await expect(objectsData.getByText('bucket1/nest2/file3.png')).toBeVisible() +}) + +async function mockApiBusAlerts(page: Page) { + const json: AlertsResponse = { + hasMore: false, + totals: { + info: 1, + warning: 1, + error: 1, + critical: 1, + }, + alerts: [ + { + id: '7f8f7c09da3010a16c2562c4487ba2bbe9753e69d773d78628c3fb31ef1ed60f', + severity: 'critical', + message: 'Slab migration failed', + data: { + error: + 'failed to upload slab for migration: 1 < 2: not enough contracts to support requested redundancy', + health: 0, + hint: 'Migration failures can be temporary, but if they persist it can eventually lead to data loss and should therefor be taken very seriously. It might be necessary to increase the MigrationSurchargeMultiplier in the gouging settings to ensure it has every chance of succeeding.', + objects: [ + { + bucket: 'test-bucket', + eTag: '6e5528d123fecd0fc9de26411251b730f1e7683e94d45f418aee407ab13049cd', + health: 0, + key: '/dir2/file4.png', + modTime: '2024-11-08T20:10:38Z', + size: 230648, + }, + { + bucket: 'bucket1', + eTag: '6e5528d123fecd0fc9de26411251b730f1e7683e94d45f418aee407ab13049cd', + health: 0, + key: '/directory-with-a-file/file3.png', + modTime: '2024-11-08T19:56:20Z', + size: 230648, + }, + { + bucket: 'bucket2', + eTag: '6e5528d123fecd0fc9de26411251b730f1e7683e94d45f418aee407ab13049cd', + health: 0, + key: '/folder/folder/file3.png', + modTime: '2024-11-13T15:56:47Z', + size: 230648, + }, + { + bucket: 'bucket2', + eTag: '6e5528d123fecd0fc9de26411251b730f1e7683e94d45f418aee407ab13049cd', + health: 0, + key: '/folder/folder/file4.png', + modTime: '2024-11-13T15:56:47Z', + size: 230648, + }, + { + bucket: 'bucket2', + eTag: '6e5528d123fecd0fc9de26411251b730f1e7683e94d45f418aee407ab13049cd', + health: 0, + key: '/folder/folder/file5.png', + modTime: '2024-11-13T15:56:47Z', + size: 230648, + }, + { + bucket: 'bucket2', + eTag: '6e5528d123fecd0fc9de26411251b730f1e7683e94d45f418aee407ab13049cd', + health: 0, + key: '/folder/folder/file6.png', + modTime: '2024-11-13T15:56:47Z', + size: 230648, + }, + { + bucket: 'bucket1', + eTag: '6e5528d123fecd0fc9de26411251b730f1e7683e94d45f418aee407ab13049cd', + health: 0, + key: '/inner-dir-with-a-file/inner-file.png', + modTime: '2024-11-08T19:45:24Z', + size: 230648, + }, + { + bucket: 'bucket1', + eTag: '6e5528d123fecd0fc9de26411251b730f1e7683e94d45f418aee407ab13049cd', + health: 0, + key: '/main-directory/directory-with-a-file/file.png', + modTime: '2024-11-08T19:00:59Z', + size: 230648, + }, + { + bucket: 'bucket1', + eTag: '6e5528d123fecd0fc9de26411251b730f1e7683e94d45f418aee407ab13049cd', + health: 0, + key: '/nest/file4.png', + modTime: '2024-11-08T19:00:59Z', + size: 230648, + }, + { + bucket: 'x', + eTag: '6e5528d123fecd0fc9de26411251b730f1e7683e94d45f418aee407ab13049cd', + health: 0, + key: '/nest/nest1.png', + modTime: '2024-11-20T15:15:53Z', + size: 230648, + }, + { + bucket: 'x', + eTag: '6e5528d123fecd0fc9de26411251b730f1e7683e94d45f418aee407ab13049cd', + health: 0, + key: '/nest/nest2.png', + modTime: '2024-11-20T15:15:53Z', + size: 230648, + }, + { + bucket: 'bucket1', + eTag: '6e5528d123fecd0fc9de26411251b730f1e7683e94d45f418aee407ab13049cd', + health: 0, + key: '/nest2/file3.png', + modTime: '2024-11-08T19:00:59Z', + size: 230648, + }, + ], + origin: 'worker.worker', + slabKey: + 'skey:7cc08070dc880ffbfecd8fa702fb73adfdcb78d6fcb68c6601606c073f49ff39', + }, + timestamp: '2024-11-26T15:10:11.760596-05:00', + }, + { + id: '288153f639d373d7f9270caf75663277664bc5b4991c7fe472daba1f77e1277a', + severity: 'info', + message: 'Contract usability updated', + data: { + churn: { + '26cd68ac42d4056f1494aef012bf9da4f753ba15e2831722eebf30a78243d534': + [ + { + from: 'bad', + to: 'good', + time: '2024-11-26T20:07:50.185945Z', + hostKey: 'hk', + size: 30000, + }, + ], + '437b0c09f6167790fefc21000c4a4a81de109729151414526562721ee7802ac6': + [ + { + from: 'good', + to: 'bad', + reason: 'host is price gouging', + time: '2024-11-26T20:07:50.185945Z', + hostKey: 'hk', + size: 4000, + }, + ], + '89dfc5594909fd468729b59096b26c886b25106e5479ceb1a28276420cb32fd3': + [ + { + from: 'good', + to: 'bad', + reason: 'host is price gouging', + time: '2024-11-26T20:07:50.185945Z', + hostKey: 'hk', + size: 10000, + }, + ], + b6f32dc39998bd85d730d39666360225af12fbad3bc18de4df50ce09073c9393: [ + { + from: 'good', + to: 'bad', + time: '2023-11-28T20:07:50.185945Z', + reason: 'host is price gouging', + hostKey: 'hk', + size: 30000000, + }, + { + from: 'bad', + to: 'good', + time: '2023-11-27T20:07:50.185945Z', + hostKey: 'hk', + size: 30000, + }, + { + from: 'good', + to: 'bad', + reason: 'host is price gouging', + time: '2023-11-26T20:07:50.185945Z', + hostKey: 'hk', + size: 30, + }, + ], + f0bbb8b6a1a6219beb510f0c4008bba9ed5687b5e617d10efce206022248ed59: [ + { + from: 'good', + to: 'bad', + reason: 'host is price gouging', + time: '2024-11-26T20:07:50.185945Z', + hostKey: 'hk', + size: 50000, + }, + ], + }, + hint: 'High usability churn can lead to a lot of unnecessary migrations, it might be necessary to tweak your configuration depending on the reason hosts are being discarded.', + }, + timestamp: '2024-11-26T15:07:50.185945-05:00', + }, + ], + } + await page.route(`**/api${busAlertsRoute}*`, async (route) => { + await route.fulfill({ json }) + }) + return json +} diff --git a/apps/renterd/components/Files/Columns/FilesHealthColumn/FilesHealthColumnContents.tsx b/apps/renterd/components/Files/Columns/FilesHealthColumn/FilesHealthColumnContents.tsx index 8ee5e9b2c..1f91f3e99 100644 --- a/apps/renterd/components/Files/Columns/FilesHealthColumn/FilesHealthColumnContents.tsx +++ b/apps/renterd/components/Files/Columns/FilesHealthColumn/FilesHealthColumnContents.tsx @@ -10,7 +10,7 @@ import { cx } from 'class-variance-authority' import { sortBy } from '@technically/lodash' import { computeSlabContractSetShards } from '../../../../lib/health' import { ObjectData } from '../../../../contexts/filesManager/types' -import { useHealthLabel } from '../../../../hooks/useHealthLabel' +import { getFileHealth } from '../../../../lib/fileHealth' import { bucketAndKeyParamsFromPath } from '../../../../lib/paths' export function FilesHealthColumnContents({ @@ -31,7 +31,7 @@ export function FilesHealthColumnContents({ }, }) - const { displayHealth, label } = useHealthLabel({ + const { displayHealth, label } = getFileHealth({ health, size, isDirectory, diff --git a/apps/renterd/components/Files/Columns/FilesHealthColumn/index.tsx b/apps/renterd/components/Files/Columns/FilesHealthColumn/index.tsx index 5c2ccf332..22c35b676 100644 --- a/apps/renterd/components/Files/Columns/FilesHealthColumn/index.tsx +++ b/apps/renterd/components/Files/Columns/FilesHealthColumn/index.tsx @@ -1,12 +1,12 @@ import { HoverCard, LoadingDots, Text } from '@siafoundation/design-system' import { ObjectData } from '../../../../contexts/filesManager/types' -import { useHealthLabel } from '../../../../hooks/useHealthLabel' +import { getFileHealth } from '../../../../lib/fileHealth' import { FilesHealthColumnContents } from './FilesHealthColumnContents' export function FilesHealthColumn(props: ObjectData) { const { name, isUploading, type, health: _health, size } = props const isDirectory = type === 'directory' - const { displayHealth, label, color, icon } = useHealthLabel({ + const { displayHealth, label, color, icon } = getFileHealth({ health: _health, size, isDirectory, diff --git a/apps/renterd/components/Files/FilesStatsMenuShared/FilesStatsMenuHealth.tsx b/apps/renterd/components/Files/FilesStatsMenuShared/FilesStatsMenuHealth.tsx index 7249f2744..d3309a57c 100644 --- a/apps/renterd/components/Files/FilesStatsMenuShared/FilesStatsMenuHealth.tsx +++ b/apps/renterd/components/Files/FilesStatsMenuShared/FilesStatsMenuHealth.tsx @@ -1,6 +1,6 @@ import { Separator, Text, Tooltip } from '@siafoundation/design-system' import { useObjectStats } from '@siafoundation/renterd-react' -import { healthThresholds, useHealthLabel } from '../../../hooks/useHealthLabel' +import { healthThresholds, getFileHealth } from '../../../lib/fileHealth' export function FilesStatsMenuHealth() { const stats = useObjectStats({ @@ -14,7 +14,7 @@ export function FilesStatsMenuHealth() { }, }) - const { displayHealth, label } = useHealthLabel({ + const { displayHealth, label } = getFileHealth({ health: stats.data?.minHealth, size: 1, isDirectory: true, diff --git a/apps/renterd/contexts/alerts/SetChange.tsx b/apps/renterd/contexts/alerts/ChurnEventsField.tsx similarity index 59% rename from apps/renterd/contexts/alerts/SetChange.tsx rename to apps/renterd/contexts/alerts/ChurnEventsField.tsx index 23c5cdb93..26d5fba5c 100644 --- a/apps/renterd/contexts/alerts/SetChange.tsx +++ b/apps/renterd/contexts/alerts/ChurnEventsField.tsx @@ -1,4 +1,9 @@ -import { Text, Tooltip, ValueCopyable } from '@siafoundation/design-system' +import { + objectEntries, + Text, + Tooltip, + ValueCopyable, +} from '@siafoundation/design-system' import { HostContextMenuFromKey } from '../../components/Hosts/HostContextMenuFromKey' import { ContractContextMenuFromId } from '../../components/Contracts/ContractContextMenuFromId' import { humanBytes } from '@siafoundation/units' @@ -6,74 +11,30 @@ import { formatRelative } from 'date-fns' import { useMemo } from 'react' import { Add16, Subtract16 } from '@siafoundation/react-icons' import { cx } from 'class-variance-authority' -import { uniq } from '@technically/lodash' - -type ChangeEvent = { - type: 'addition' | 'removal' - reasons?: string - size: number - time: string -} +import { AlertChurnEvent } from '@siafoundation/renterd-types' type Change = { contractId: string hostKey: string - events: ChangeEvent[] + events: AlertChurnEvent[] } -export type SetAdditions = Record< - string, - { - hostKey: string - additions: { size: number; time: string }[] - } -> - -export type SetRemovals = Record< - string, - { - hostKey: string - removals: { reasons: string; size: number; time: string }[] - } -> +export type ChurnData = Record -export function SetChangesField({ - setAdditions, - setRemovals, +export function ChurnEventsField({ + data, }: { - setAdditions: SetAdditions - setRemovals: SetRemovals + data: Record }) { - const changes = useMemo(() => { - // Merge all unique contract ids from additions and removals together - const contractIds = uniq([ - ...Object.keys(setAdditions), - ...Object.keys(setRemovals), - ]) - return contractIds - .map((contractId) => { - const additions = setAdditions[contractId]?.additions || [] - const removals = setRemovals[contractId]?.removals || [] + const churnEvents = useMemo(() => { + return objectEntries(data) + .map(([contractId, events]) => { return { contractId, - hostKey: - setAdditions[contractId]?.hostKey || - setRemovals[contractId]?.hostKey, - events: [ - ...additions.map((a) => ({ - type: 'addition', - size: a.size, - time: a.time, - })), - ...removals.map((r) => ({ - type: 'removal', - size: r.size, - time: r.time, - reasons: r.reasons, - })), - ].sort((a, b) => + hostKey: events[0].hostKey, + events: events.sort((a, b) => new Date(a.time).getTime() < new Date(b.time).getTime() ? 1 : -1 - ) as ChangeEvent[], + ), } }) .sort((a, b) => { @@ -82,39 +43,39 @@ export function SetChangesField({ const bSize = b.events[0].size return aSize < bSize ? 1 : -1 }) - }, [setAdditions, setRemovals]) + }, [data]) - // calculate churn %: contracts removed size / total size + // calculate churn %: contracts bad size / total size const totalSize = useMemo( - () => changes.reduce((acc, { events }) => acc + events[0].size, 0), - [changes] + () => churnEvents.reduce((acc, { events }) => acc + events[0].size, 0), + [churnEvents] ) - const removals = useMemo( - () => changes.filter(({ events }) => events[0].type === 'removal'), - [changes] + const bads = useMemo( + () => churnEvents.filter(({ events }) => events[0].to === 'bad'), + [churnEvents] ) - const additions = useMemo( - () => changes.filter(({ events }) => events[0].type === 'addition'), - [changes] + const goods = useMemo( + () => churnEvents.filter(({ events }) => events[0].to === 'good'), + [churnEvents] ) - const removedSize = useMemo( - () => removals.reduce((acc, { events }) => acc + events[0].size, 0), - [removals] + const badSize = useMemo( + () => bads.reduce((acc, { events }) => acc + events[0].size, 0), + [bads] ) const churn = useMemo( - () => (totalSize > 0 ? (removedSize / totalSize) * 100 : 0), - [removedSize, totalSize] + () => (totalSize > 0 ? (badSize / totalSize) * 100 : 0), + [badSize, totalSize] ) return ( -
+
- contract set changes + contract changes
@@ -123,12 +84,12 @@ export function SetChangesField({ churn: {churn.toFixed(2)}% - ({humanBytes(removedSize)} / {humanBytes(totalSize)}) + ({humanBytes(badSize)} / {humanBytes(totalSize)})
- + - {additions.length} + {goods.length} - + - {removals.length} + {bads.length}
- {changes.map(({ contractId, hostKey, events }, i) => ( - ( + +
{i + 1}. @@ -219,10 +180,10 @@ function ContractSetChange({ />
- {events.map(({ type, reasons, size, time }, i) => ( + {events.map(({ to, reason, size, time }, i) => ( @@ -230,18 +191,18 @@ function ContractSetChange({ className={cx( 'flex gap-2 justify-between mr-2 pr-1', i === 0 - ? type === 'addition' + ? to === 'good' ? 'bg-green-400/20' : 'bg-red-400/20' : 'opacity-50' )} >
- - {type === 'addition' ? : } + + {to === 'good' ? : } - {reasons} + {reason}
diff --git a/apps/renterd/contexts/alerts/columns.tsx b/apps/renterd/contexts/alerts/columns.tsx index 77862e7db..c4db94f8a 100644 --- a/apps/renterd/contexts/alerts/columns.tsx +++ b/apps/renterd/contexts/alerts/columns.tsx @@ -9,7 +9,6 @@ import { } from '@siafoundation/design-system' import { AlertData, TableColumnId } from './types' import { dataFields } from './data' -import { SetAdditions, SetChangesField, SetRemovals } from './SetChange' import { Checkmark16 } from '@siafoundation/react-icons' import { formatRelative } from 'date-fns' import { Fragment, useMemo } from 'react' @@ -102,32 +101,9 @@ export const columns: AlertsTableColumn[] = [ .filter(Boolean) as { key: string; value: unknown }[], [data] ) - // Collect set changes for the custom SetChangeField component - // which is a special case that combines two keys of data - const setAdditions = useMemo( - () => data['setAdditions'] as SetAdditions, - [data] - ) - const setRemovals = useMemo( - () => data['setRemovals'] as SetRemovals, - [data] - ) return (
- {(setAdditions || setRemovals) && ( - -
- -
- {datums.length >= 1 && ( - - )} -
- )} {datums.map(({ key, value }, i) => { const Component = dataFields?.[key]?.render if (!Component) { diff --git a/apps/renterd/contexts/alerts/data.tsx b/apps/renterd/contexts/alerts/data.tsx index be97a0ea6..3b424494b 100644 --- a/apps/renterd/contexts/alerts/data.tsx +++ b/apps/renterd/contexts/alerts/data.tsx @@ -3,6 +3,7 @@ import { Link, ScrollArea, Text, + Tooltip, ValueCopyable, ValueMenu, ValueNum, @@ -18,15 +19,17 @@ import { ContractContextMenuFromId } from '../../components/Contracts/ContractCo import { AccountContextMenu } from '../../components/AccountContextMenu' import { FileContextMenu } from '../../components/Files/FileContextMenu' import { CaretDown16 } from '@siafoundation/react-icons' +import { ChurnEventsField } from './ChurnEventsField' +import { AlertData } from '@siafoundation/renterd-types' +import { getFileHealth } from '../../lib/fileHealth' -type Render = (props: { value: unknown }) => JSX.Element - -export const dataFields: Record< - string, - { render: (props: { value: unknown }) => JSX.Element } -> = { +export const dataFields: { + [K in keyof AlertData]: { + render: (props: { value: NonNullable }) => JSX.Element | null + } +} = { origin: { - render: function OriginField({ value }: { value: string }) { + render({ value }) { return (
@@ -37,10 +40,10 @@ export const dataFields: Record<
) - } as Render, + }, }, contractID: { - render: function ContractField({ value }: { value: string }) { + render({ value }) { return (
@@ -63,10 +66,10 @@ export const dataFields: Record< />
) - } as Render, + }, }, accountID: { - render: function AccountField({ value }: { value: string }) { + render({ value }) { return (
@@ -90,10 +93,10 @@ export const dataFields: Record< />
) - } as Render, + }, }, hostKey: { - render: function HostField({ value }: { value: string }) { + render: function HostKey({ value }) { const host = useHost({ params: { hostkey: value } }) if (!host.data) { return null @@ -121,10 +124,10 @@ export const dataFields: Record< />
) - } as Render, + }, }, slabKey: { - render: function SlabField({ value }: { value: string }) { + render({ value }) { return (
@@ -133,12 +136,12 @@ export const dataFields: Record<
) - } as Render, + }, }, health: { - render: function OriginField({ value }: { value: string }) { + render({ value }) { return ( -
+
health @@ -147,34 +150,46 @@ export const dataFields: Record<
) - } as Render, + }, }, - objectIDs: { - render: function ObjectIdsField({ - value, - }: { - value: Record - }) { + objects: { + render: function ObjectIDs({ value }) { const { setActiveDirectory } = useFilesManager() const { closeDialog } = useDialog() return ( -
-
+
+
- object IDs + objects + + + {value.length}
-
+
- {Object.entries(value).map(([bucket, paths]) => - paths.map((path) => { - const fullPath = `${bucket}${path}` - return ( -
+ {value.map(({ bucket, key, health, size }) => { + const fullPath = `${bucket}${key}` + const { color, icon, label } = getFileHealth({ + health, + size, + isDirectory: key.endsWith('/'), + }) + return ( +
+
+ + + {icon} + + {fullPath} - - - - } - />
- ) - }) - )} + + + + } + /> +
+ ) + })}
) - } as Render, + }, }, added: { - render: function Component({ value }: { value: number }) { + render({ value }) { return (
@@ -229,10 +244,10 @@ export const dataFields: Record<
) - } as Render, + }, }, removed: { - render: function Component({ value }: { value: number }) { + render({ value }) { return (
@@ -243,10 +258,10 @@ export const dataFields: Record<
) - } as Render, + }, }, migrationsInterrupted: { - render: function Component({ value }: { value: string }) { + render({ value }) { return (
@@ -257,10 +272,10 @@ export const dataFields: Record<
) - } as Render, + }, }, balance: { - render: function Component({ value }: { value: string }) { + render({ value }) { return (
@@ -269,10 +284,10 @@ export const dataFields: Record<
) - } as Render, + }, }, address: { - render: function Component({ value }: { value: string }) { + render({ value }) { return (
@@ -281,10 +296,10 @@ export const dataFields: Record<
) - } as Render, + }, }, account: { - render: function Component({ value }: { value: string }) { + render({ value }) { return (
@@ -308,10 +323,10 @@ export const dataFields: Record< />
) - } as Render, + }, }, lostSectors: { - render: function Component({ value }: { value: number }) { + render({ value }) { return (
@@ -325,6 +340,11 @@ export const dataFields: Record< />
) - } as Render, + }, + }, + churn: { + render({ value }) { + return + }, }, } diff --git a/apps/renterd/hooks/useHealthLabel.tsx b/apps/renterd/lib/fileHealth.tsx similarity index 97% rename from apps/renterd/hooks/useHealthLabel.tsx rename to apps/renterd/lib/fileHealth.tsx index d57c9c10c..9e0ac134d 100644 --- a/apps/renterd/hooks/useHealthLabel.tsx +++ b/apps/renterd/lib/fileHealth.tsx @@ -11,7 +11,7 @@ export const healthThresholds = { poor: 0, } -export function useHealthLabel({ +export function getFileHealth({ health = 0, size, isDirectory, diff --git a/libs/renterd-types/src/bus.ts b/libs/renterd-types/src/bus.ts index 169c7c4c3..46b46d3b1 100644 --- a/libs/renterd-types/src/bus.ts +++ b/libs/renterd-types/src/bus.ts @@ -458,16 +458,41 @@ export type ObjectsStatsResponse = { export type AlertSeverity = 'info' | 'warning' | 'error' | 'critical' +export type AlertChurnEvent = { + from: 'good' | 'bad' + to: 'good' | 'bad' + reason?: string + size: number + hostKey: string + time: string +} + +export type AlertData = { + error?: string + hint?: string + origin?: string + contractID?: string + accountID?: string + hostKey?: string + slabKey?: string + health?: number + objects?: ObjectMetadata[] + added?: number + removed?: number + migrationsInterrupted?: string + balance?: string + address?: string + account?: string + lostSectors?: number + churn?: Record +} + export type Alert = { id: string severity: AlertSeverity message: string timestamp: string - data: { - account?: string - host?: string - key?: string - } + data: AlertData } export type AlertsParams = {