Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hostd): upgraded alerts feature #879

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/forty-gifts-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'hostd': minor
---

Alerts can now be accessed via the cmd+k menu.
5 changes: 5 additions & 0 deletions .changeset/four-avocados-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/design-system': minor
---

Removed AlertsDialog.
5 changes: 5 additions & 0 deletions .changeset/short-mayflies-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/hostd-types': minor
---

Added named AlertData type.
5 changes: 5 additions & 0 deletions .changeset/smart-turkeys-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siafoundation/e2e': minor
---

Added continueToClickUntil.
5 changes: 5 additions & 0 deletions .changeset/spicy-tips-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'hostd': minor
---

The hostd alerts feature is now a full page and matches the user experience of renterd alerts.
24 changes: 24 additions & 0 deletions apps/hostd-e2e/src/fixtures/alerts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Page } from '@playwright/test'
import { maybeExpectAndReturn, step } from '@siafoundation/e2e'

export const getAlertRows = (page: Page) => {
return page.getByTestId('alertsTable').locator('tbody').getByRole('row')
}

export const getAlertRowsAll = step('get alert rows', async (page: Page) => {
return getAlertRows(page).all()
})

export const getAlertRowByIndex = step(
'get alert row by index',
async (page: Page, index: number, shouldExpect?: boolean) => {
return maybeExpectAndReturn(
page
.getByTestId('alertsTable')
.locator('tbody')
.getByRole('row')
.nth(index),
shouldExpect
)
}
)
8 changes: 3 additions & 5 deletions apps/hostd-e2e/src/fixtures/navigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,10 @@ export const navigateToContracts = step(
}
)

export const openAlertsDialog = step(
'open alerts dialog',
export const navigateToAlerts = step(
'navigate to alerts',
async (page: Page) => {
await page.getByTestId('sidenav').getByLabel('Alerts').click()
const dialog = page.getByRole('dialog')
await expect(dialog.getByText('Alerts')).toBeVisible()
return dialog
await expect(page.getByTestId('navbar').getByText('Alerts')).toBeVisible()
}
)
90 changes: 90 additions & 0 deletions apps/hostd-e2e/src/specs/alerts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { test, expect, Page } from '@playwright/test'
import { navigateToAlerts, navigateToVolumes } from '../fixtures/navigate'
import { afterTest, beforeTest } from '../fixtures/beforeTest'
import { Alert } from '@siafoundation/hostd-types'
import { getAlertRows } from '../fixtures/alerts'
import { createVolume, deleteVolume } from '../fixtures/volumes'
import fs from 'fs'
import os from 'os'
import { continueToClickUntil } from '@siafoundation/e2e'

let dirPath = '/'

test.beforeEach(async ({ page }) => {
await beforeTest(page)
// Create a temporary directory.
dirPath = fs.mkdtempSync(process.env.GITHUB_WORKSPACE || os.tmpdir())
})

test.afterEach(async () => {
await afterTest()
try {
fs.rmSync(dirPath, { recursive: true })
} catch (e) {
console.error(e)
}
})

test('filtering alerts', async ({ page }) => {
await mockApiAlerts(page)
await page.reload()

await navigateToAlerts(page)

// Check initial number of alerts.
await expect(getAlertRows(page)).toHaveCount(2)

// Verify alert content.
await expect(page.getByText('Volume initialized')).toBeVisible()
await expect(page.getByText('Volume warning')).toBeVisible()

// Test filtering.
await page.getByRole('button', { name: 'Info' }).click()
await expect(getAlertRows(page)).toHaveCount(1)
})

test('dismissing alerts', async ({ page }) => {
const name = 'my-new-volume'
await navigateToVolumes({ page })
await createVolume(page, name, dirPath)
await navigateToAlerts(page)
await expect(getAlertRows(page).getByText('Volume initialized')).toBeVisible()
// Dismissing the alert too early will cause the alert to reappear
// so maybe try to dismiss more than once.
await continueToClickUntil(
page.getByRole('button', { name: 'dismiss alert' }),
page.getByText('There are currently no alerts.')
)
await navigateToVolumes({ page })
await deleteVolume(page, name, dirPath)
})

async function mockApiAlerts(page: Page) {
const alerts: Alert[] = [
{
id: 'c39d09ee61a5d1dd9ad97015a0e87e9286f765bbf109cafad936d5a1aa843e54',
severity: 'info',
message: 'Volume initialized',
data: {
elapsed: 95823333,
target: 2623,
volumeID: 3,
},
timestamp: '2025-01-10T10:17:52.365323-05:00',
},
{
id: '93683b58d12c2de737a8849561b9f0dae07120eee3185f40da489d48585b416a',
severity: 'warning',
message: 'Volume warning',
data: {
elapsed: 257749500,
targetSectors: 7868,
volumeID: 2,
},
timestamp: '2025-01-10T10:17:18.754568-05:00',
},
]
await page.route('**/api/alerts', async (route) => {
await route.fulfill({ json: alerts })
})
}
15 changes: 10 additions & 5 deletions apps/hostd-e2e/src/specs/volumes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test'
import { navigateToVolumes, openAlertsDialog } from '../fixtures/navigate'
import { navigateToVolumes, navigateToAlerts } from '../fixtures/navigate'
import {
createVolume,
deleteVolume,
Expand All @@ -13,6 +13,7 @@ import fs from 'fs'
import os from 'os'
import { fillTextInputByName } from '@siafoundation/e2e'
import path from 'path'
import { getAlertRows } from '../fixtures/alerts'

let dirPath = '/'

Expand All @@ -24,16 +25,20 @@ test.beforeEach(async ({ page }) => {

test.afterEach(async () => {
await afterTest()
fs.rmSync(dirPath, { recursive: true })
try {
fs.rmSync(dirPath, { recursive: true })
} catch (e) {
console.error(e)
}
})

test('can create and delete a volume', async ({ page }) => {
const name = 'my-new-volume'
await navigateToVolumes({ page })
await createVolume(page, name, dirPath)
const dialog = await openAlertsDialog(page)
await expect(dialog.getByText('Volume initialized')).toBeVisible()
await dialog.getByLabel('close').click()
await navigateToAlerts(page)
await expect(getAlertRows(page).getByText('Volume initialized')).toBeVisible()
await navigateToVolumes({ page })
await deleteVolume(page, name, dirPath)
})

Expand Down
54 changes: 54 additions & 0 deletions apps/hostd/components/Alerts/AlertContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
DropdownMenu,
DropdownMenuItem,
Button,
DropdownMenuLeftSlot,
DropdownMenuLabel,
Text,
} from '@siafoundation/design-system'
import { CaretDown16, Checkmark16 } from '@siafoundation/react-icons'
import { useAlerts } from '../../contexts/alerts'

type Props = {
id: string
contentProps?: React.ComponentProps<typeof DropdownMenu>['contentProps']
buttonProps?: React.ComponentProps<typeof Button>
}

export function AlertContextMenu({ id, contentProps, buttonProps }: Props) {
const { dismissOne } = useAlerts()

return (
<DropdownMenu
trigger={
<Button variant="ghost" icon="hover" {...buttonProps}>
<CaretDown16 />
</Button>
}
contentProps={{
align: 'start',
...contentProps,
onClick: (e) => {
e.stopPropagation()
},
}}
>
<div className="px-1.5 py-1">
<Text size="14" weight="medium" color="subtle">
Alert {id.slice(0, 24)}...
</Text>
</div>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onSelect={() => {
dismissOne(id)
}}
>
<DropdownMenuLeftSlot>
<Checkmark16 />
</DropdownMenuLeftSlot>
Clear alert
</DropdownMenuItem>
</DropdownMenu>
)
}
9 changes: 9 additions & 0 deletions apps/hostd/components/Alerts/AlertsActionsMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AlertsViewDropdownMenu } from './AlertsViewDropdownMenu'

export function AlertsActionsMenu() {
return (
<div className="flex gap-2">
<AlertsViewDropdownMenu />
</div>
)
}
55 changes: 55 additions & 0 deletions apps/hostd/components/Alerts/AlertsCmd/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
CommandGroup,
CommandItemNav,
CommandItemSearch,
} from '../../CmdRoot/Item'
import { Page } from '../../CmdRoot/types'
import { useRouter } from 'next/router'
import { useDialog } from '../../../contexts/dialog'
import { routes } from '../../../config/routes'

export const commandPage = {
namespace: 'alerts',
label: 'Alerts',
}

export function AlertsCmd({
currentPage,
parentPage,
pushPage,
}: {
currentPage: Page
parentPage?: Page
beforeSelect?: () => void
afterSelect?: () => void
pushPage: (page: Page) => void
}) {
const router = useRouter()
const { closeDialog } = useDialog()
return (
<>
<CommandItemNav
currentPage={currentPage}
parentPage={parentPage}
commandPage={parentPage}
onSelect={() => {
pushPage(commandPage)
}}
>
{commandPage.label}
</CommandItemNav>
<CommandGroup currentPage={currentPage} commandPage={commandPage}>
<CommandItemSearch
currentPage={currentPage}
commandPage={commandPage}
onSelect={() => {
router.push(routes.alerts.index)
closeDialog()
}}
>
View alerts
</CommandItemSearch>
</CommandGroup>
</>
)
}
Loading
Loading