Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fe5e6e5
upcoming: [DI-23311] - Added reusable component for alerts contextual…
nikhagra-akamai Feb 19, 2025
819dd5b
added changeset
nikhagra-akamai Feb 19, 2025
9897792
upcoming:[DI-23311] - Remove unused variable
nikhagra-akamai Feb 19, 2025
a276828
Merge branch 'develop' of github.com:linode/manager into alerts/conte…
nikhagra-akamai Feb 20, 2025
c9ba840
upcoming: [DI-23548] - Updated test case
nikhagra-akamai Feb 20, 2025
8b6e0ca
upcoming: [DI-23548] - Remove unused code
nikhagra-akamai Feb 21, 2025
2326c15
upcoming: [DI-23548] - Hide label in search & autocomplete
nikhagra-akamai Feb 21, 2025
97dc48b
upcoming: [DI-23548] - Removed unused variable
nikhagra-akamai Feb 21, 2025
1ed1b70
Merge branch develop of github.com:linode/manager into alerts/context…
nikhagra-akamai Feb 25, 2025
a16186e
upcoming: [DI-23548] - Updated button type to outlined
nikhagra-akamai Feb 25, 2025
1808d31
Merge branch 'develop' of github.com:linode/manager into alerts/conte…
nikhagra-akamai Feb 26, 2025
3621f00
upcoming: [DI-23640] - Added table in alert contextual view
nikhagra-akamai Feb 26, 2025
a473fc5
Merge branch develop of github.com:linode/manager into alerts/context…
nikhagra-akamai Feb 26, 2025
0df6220
added changeset
nikhagra-akamai Feb 26, 2025
7422394
upcoming: [DI-23640] - Replace null component with hyphen
nikhagra-akamai Feb 26, 2025
dd6e8ac
upcoming: [DI-23640] - Removed unused variables
nikhagra-akamai Feb 26, 2025
b472352
upcoming: [DI-23640] - Removed unused imports
nikhagra-akamai Feb 26, 2025
5eae251
upcoming: [DI-23640] - Updated chip styling
nikhagra-akamai Feb 27, 2025
6acb83c
Merge branch 'develop' of github.com:linode/manager into alerts/conte…
nikhagra-akamai Feb 27, 2025
9fa05eb
upcoming: [DI-23640] - Updated test cases
nikhagra-akamai Feb 27, 2025
19bd613
Added testing file
nikhagra-akamai Feb 27, 2025
d2684ae
upcoming: [DI-23640] - Updated review comments
nikhagra-akamai Feb 28, 2025
085aff1
upcoming: [DI-23640] - Updated color for chip component
nikhagra-akamai Feb 28, 2025
2de5899
reverted index file
nikhagra-akamai Mar 5, 2025
d270be7
Merge branch 'develop' of github.com:linode/manager into alerts/conte…
nikhagra-akamai Mar 5, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Update `AlertReusableComponent` to add table, Add `AlertInformationActionTable` and `AlertInformationActionRow` compoenent ([#11734](https://github.com/linode/manager/pull/11734))
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { TableColumnHeader } from '../ContextualView/AlertInformationActionTable';
import type { AlertStatusType, AlertStatusUpdateType } from '@linode/api-v4';

export const AlertListingTableLabelMap = [
Expand Down Expand Up @@ -30,3 +31,9 @@ export const statusToActionMap: Record<
disabled: 'Enable',
enabled: 'Disable',
};

export const AlertContextualViewTableHeaderMap: TableColumnHeader[] = [
{ columnName: 'Alert Name', label: 'label' },
{ columnName: 'Metric Threshold', label: 'id' },
{ columnName: 'Alert Type', label: 'type' },
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { capitalize } from '@linode/utilities';
import React from 'react';

import { alertFactory } from 'src/factories';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { processMetricCriteria } from '../Utils/utils';
import { AlertInformationActionRow } from './AlertInformationActionRow';

describe('Alert list table row', () => {
it('Should display the data', () => {
const alert = alertFactory.build();

const { getByText } = renderWithTheme(
<AlertInformationActionRow
alert={alert}
handleToggle={vi.fn()}
status={true}
/>
);

expect(getByText(alert.label)).toBeInTheDocument();
expect(getByText(capitalize(alert.type))).toBeInTheDocument();
});

it('Should display metric threshold', () => {
const alert = alertFactory.build();
const processCriteria = processMetricCriteria(alert.rule_criteria.rules)[0];
const { getByText } = renderWithTheme(
<AlertInformationActionRow
alert={alert}
handleToggle={vi.fn()}
status={false}
/>
);
expect(
getByText(
`${processCriteria.label} ${processCriteria.metricOperator} ${processCriteria.threshold} ${processCriteria.unit}`
)
).toBeInTheDocument();
});

it('Should have toggle button disabled', () => {
const alert = alertFactory.build();
const { getByRole } = renderWithTheme(
<AlertInformationActionRow
alert={alert}
handleToggle={vi.fn()}
status={true}
/>
);

expect(getByRole('checkbox')).toHaveProperty('checked');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { FormControlLabel, Toggle, Typography } from '@linode/ui';
import { capitalize } from '@linode/utilities';
import React from 'react';

import { Link } from 'src/components/Link';
import { TableCell } from 'src/components/TableCell';
import { TableRow } from 'src/components/TableRow';

import { processMetricCriteria } from '../Utils/utils';
import { MetricThreshold } from './MetricThreshold';

import type { Alert } from '@linode/api-v4';

interface AlertInformationActionRowProps {
/**
* Alert object which should be dispalyed in the row
*/
alert: Alert;

/**
* Handler function for the click of toggle button
* @param alert object for which toggle button is click
*/
handleToggle: (alert: Alert) => void;

/**
* Status for the alert whether it is enabled or disabled
*/
status?: boolean;
}

export const AlertInformationActionRow = (
props: AlertInformationActionRowProps
) => {
const { alert, handleToggle, status = false } = props;
const { id, label, rule_criteria, service_type, type } = alert;
const metricThreshold = processMetricCriteria(rule_criteria.rules);

return (
<TableRow data-qa-alert-cell={id} data-testid={id} key={`alert-row-${id}`}>
<TableCell sx={{ width: 0 }}>
<FormControlLabel
control={
<Toggle checked={status} onChange={() => handleToggle(alert)} />
}
label={''}
/>
</TableCell>
<TableCell>
<Link to={`/monitor/alerts/definitions/detail/${service_type}/${id}`}>
{label}
</Link>
</TableCell>
<TableCell>
<MetricThreshold metricThreshold={metricThreshold} />
</TableCell>
<TableCell>
<Typography
sx={(theme) => ({
font: theme.tokens.typography.Label.Regular.S,
})}
>
{capitalize(type)}
</Typography>
</TableCell>
</TableRow>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { within } from '@testing-library/react';
import React from 'react';

import { alertFactory } from 'src/factories/cloudpulse/alerts';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { AlertInformationActionTable } from './AlertInformationActionTable';

import type {
AlertInformationActionTableProps,
TableColumnHeader,
} from './AlertInformationActionTable';

const serviceType = 'linode';
const entityId = '123';
const entityName = 'test-instance';
const alerts = [
...alertFactory.buildList(7, {
entity_ids: [entityId],
service_type: serviceType,
status: 'enabled',
}),
];
const columns: TableColumnHeader[] = [
{ columnName: 'Alert Name', label: 'label' },
{ columnName: 'Metric Threshold', label: 'id' },
{ columnName: 'Alert Type', label: 'type' },
];
const props: AlertInformationActionTableProps = {
alerts,
columns,
entityId,
entityName,
orderByColumn: 'Alert Name',
};

describe('Alert Listing Reusable Table for contextual view', () => {
it('Should render alert table', () => {
const { getByText } = renderWithTheme(
<AlertInformationActionTable {...props} />
);

expect(getByText('Alert Name')).toBeInTheDocument();
expect(getByText('Metric Threshold')).toBeInTheDocument();
expect(getByText('Alert Type')).toBeInTheDocument();
});

it('Should show message for empty table', () => {
const { getByText } = renderWithTheme(
<AlertInformationActionTable {...props} alerts={[]} />
);

expect(getByText('No data to display.')).toBeInTheDocument();
});

it('Shoud render table row toggle in table row', async () => {
const { findByTestId } = renderWithTheme(
<AlertInformationActionTable {...props} />
);
const alert = alerts[0];
const row = await findByTestId(alert.id);

const checkbox = await within(row).findByRole('checkbox');

expect(checkbox).toHaveProperty('checked');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { Box } from '@linode/ui';
import { Grid, TableBody, TableHead } from '@mui/material';
import React from 'react';

import OrderBy from 'src/components/OrderBy';
import Paginate from 'src/components/Paginate';
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
import { Table } from 'src/components/Table';
import { TableCell } from 'src/components/TableCell';
import { TableContentWrapper } from 'src/components/TableContentWrapper/TableContentWrapper';
import { TableRow } from 'src/components/TableRow';
import { TableSortCell } from 'src/components/TableSortCell';
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';

import { AlertInformationActionRow } from './AlertInformationActionRow';

import type { APIError, Alert } from '@linode/api-v4';

export interface AlertInformationActionTableProps {
/**
* List of alerts to be displayed
*/
alerts: Alert[];

/**
* List of table headers for each column
*/
columns: TableColumnHeader[];

/**
* Id of the selected entity
*/
entityId: string;

/**
* Name of the selected entity
*/
entityName: string;

/**
* Error received from API
*/
error?: APIError[] | null;

/**
* Column name by which columns will be ordered by default
*/
orderByColumn: string;
}

export interface TableColumnHeader {
/**
* Name of the column to be displayed
*/
columnName: string;

/**
* Corresponding key name in the alert object for which this column is
*/
label: string;
}

export interface AlertStatusMap {
/**
* Alert id to boolean mapping to hold whether that alert id is enabled or not
*/
[alertId: number]: boolean;
}

// generate the mapping of id to whether entity present in entity_ids list or not for quick access in table row toggles
const generateStatusMap = (
alerts: Alert[],
entityId: string
): AlertStatusMap => {
return alerts.reduce<AlertStatusMap>(
(previousValue, alert) => ({
...previousValue,
[alert.id]: alert.entity_ids.includes(entityId),
}),
{}
);
};
Comment on lines +71 to +82

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should do this after reviewing. The mapping is only used for the initial state of alertStatusMap. After that, the component maintains its own state with handleToggle that updates this mapping without any connection to the original alert data.

A better approach would be to add a new React Query mutation to synchronize the data and "optimistically" update the new value. We do this is several places in the app https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates#optimistic-updates

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll add this approach in the next PR in which I'll be adding 2 react queries that are related to toggle button along with confirmation dialogue


export const AlertInformationActionTable = (
props: AlertInformationActionTableProps
) => {
const { alerts, columns, entityId, error, orderByColumn } = props;
const [alertStatusMap, setAlertStatusMap] = React.useState<AlertStatusMap>(
generateStatusMap(alerts, entityId)
);

const _error = error
? getAPIErrorOrDefault(error, 'Error while fetching the alerts')
: undefined;

const handleToggle = ({ id }: Alert) => {
setAlertStatusMap((previousValue) => {
return {
...previousValue,
[id]: !previousValue[id],
};
});
};
return (
<OrderBy data={alerts} order="asc" orderBy={orderByColumn}>
{({ data: orderedData, handleOrderChange, order, orderBy }) => (
<Paginate data={orderedData}>
{({
count,
data: paginatedAndOrderedAlerts,
handlePageChange,
handlePageSizeChange,
page,
pageSize,
}) => (
<Box>
<Grid>
<Table
colCount={columns.length + 1}
data-testid="alert-table"
size="small"
>
<TableHead>
<TableRow>
<TableCell actionCell />
{columns.map(({ columnName, label }) => {
return (
<TableSortCell
active={orderBy === label}
data-qa-header={label}
data-qa-sorting={label}
direction={order}
handleClick={handleOrderChange}
key={label}
label={label}
>
{columnName}
</TableSortCell>
);
})}
</TableRow>
</TableHead>
<TableBody>
<TableContentWrapper
error={_error}
length={paginatedAndOrderedAlerts.length}
loading={false}
/>
{paginatedAndOrderedAlerts?.map((alert) => (
<AlertInformationActionRow
alert={alert}
handleToggle={handleToggle}
key={alert.id}
status={alertStatusMap[alert.id]}
/>
))}
</TableBody>
</Table>
</Grid>
<PaginationFooter
count={count}
eventCategory="Alert Definitions Table"
handlePageChange={handlePageChange}
handleSizeChange={handlePageSizeChange}
page={page}
pageSize={pageSize}
/>
</Box>
)}
</Paginate>
)}
</OrderBy>
);
};
Loading