mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: Alerts page styling (#172)
This commit is contained in:
parent
6d3cdae914
commit
70f5fc4c9a
6 changed files with 330 additions and 155 deletions
5
.changeset/lemon-ads-collect.md
Normal file
5
.changeset/lemon-ads-collect.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hyperdx/app': patch
|
||||
---
|
||||
|
||||
Alerts page styling
|
||||
|
|
@ -1,46 +1,85 @@
|
|||
import * as React from 'react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import cx from 'classnames';
|
||||
import { formatRelative } from 'date-fns';
|
||||
import {
|
||||
Alert as MAlert,
|
||||
Badge,
|
||||
Button,
|
||||
Container,
|
||||
Divider,
|
||||
Group,
|
||||
Stack,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
|
||||
import api from './api';
|
||||
import AppNav from './AppNav';
|
||||
import type { Alert, AlertHistory, LogView } from './types';
|
||||
import { AlertState } from './types';
|
||||
|
||||
import styles from '../styles/AlertsPage.module.scss';
|
||||
|
||||
type AlertData = Alert & {
|
||||
history: AlertHistory[];
|
||||
dashboard?: {
|
||||
_id: string;
|
||||
name: string;
|
||||
charts: { id: string; name: string }[];
|
||||
};
|
||||
logView?: LogView;
|
||||
};
|
||||
|
||||
const DISABLE_ALERTS_ENABLED = false;
|
||||
|
||||
function AlertHistoryCard({ history }: { history: AlertHistory }) {
|
||||
const start = new Date(history.createdAt.toString());
|
||||
const latestValues = history.lastValues
|
||||
.map(({ count }, index) => {
|
||||
return count.toString();
|
||||
})
|
||||
.join(', ');
|
||||
const today = React.useMemo(() => new Date(), []);
|
||||
const latestHighestValue = history.lastValues.length
|
||||
? Math.max(...history.lastValues.map(({ count }) => count))
|
||||
: 0;
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'badge ' +
|
||||
(history.state === AlertState.OK ? 'bg-success ' : 'bg-danger ') +
|
||||
' m-0 rounded-0'
|
||||
}
|
||||
title={latestValues + ' ' + formatRelative(start, new Date())}
|
||||
<Tooltip
|
||||
label={latestHighestValue + ' ' + formatRelative(start, today)}
|
||||
color="dark"
|
||||
withArrow
|
||||
>
|
||||
{history.state === AlertState.OK ? '.' : '!'}
|
||||
</div>
|
||||
<div
|
||||
className={cx(
|
||||
styles.historyCard,
|
||||
history.state === AlertState.OK ? styles.ok : styles.alarm,
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const HISTORY_ITEMS = 18;
|
||||
|
||||
function AlertHistoryCardList({ history }: { history: AlertHistory[] }) {
|
||||
const items = React.useMemo(() => {
|
||||
if (history.length < HISTORY_ITEMS) {
|
||||
return history;
|
||||
}
|
||||
return history.slice(0, HISTORY_ITEMS);
|
||||
}, [history]);
|
||||
|
||||
const paddingItems = React.useMemo(() => {
|
||||
if (history.length > HISTORY_ITEMS) {
|
||||
return [];
|
||||
}
|
||||
return new Array(HISTORY_ITEMS - history.length).fill(null);
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-row">
|
||||
{history.map((history, index) => (
|
||||
<div className={styles.historyCardWrapper}>
|
||||
{paddingItems.map((_, index) => (
|
||||
<Tooltip label="No data" color="dark" withArrow key={index}>
|
||||
<div className={styles.historyCard} />
|
||||
</Tooltip>
|
||||
))}
|
||||
{items.reverse().map((history, index) => (
|
||||
<AlertHistoryCard key={index} history={history} />
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -54,114 +93,108 @@ function disableAlert(alertId?: string) {
|
|||
// TODO do some lovely disabling of the alert here
|
||||
}
|
||||
|
||||
function AlertDetails({
|
||||
alert,
|
||||
history,
|
||||
}: {
|
||||
alert: AlertData;
|
||||
history: AlertHistory[];
|
||||
}) {
|
||||
// TODO enable once disable handler is implemented above
|
||||
const showDisableButton = false;
|
||||
function AlertDetails({ alert }: { alert: AlertData }) {
|
||||
const alertName = React.useMemo(() => {
|
||||
if (alert.source === 'CHART' && alert.dashboard) {
|
||||
const chartName = alert.dashboard.charts.find(
|
||||
chart => chart.id === alert.chartId,
|
||||
)?.name;
|
||||
return (
|
||||
<>
|
||||
{alert.dashboard.name}
|
||||
{chartName ? (
|
||||
<>
|
||||
<i className="bi bi-chevron-right fs-8 mx-1 text-slate-400" />
|
||||
{chartName}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (alert.source === 'LOG' && alert.logView) {
|
||||
return alert.logView?.name;
|
||||
}
|
||||
return '–';
|
||||
}, [alert]);
|
||||
|
||||
const alertUrl = React.useMemo(() => {
|
||||
if (alert.source === 'CHART' && alert.dashboard) {
|
||||
return `/dashboards/${alert.dashboard._id}?highlightedChartId=${alert.chartId}`;
|
||||
}
|
||||
if (alert.source === 'LOG' && alert.logView) {
|
||||
return `/search/${alert.logView._id}`;
|
||||
}
|
||||
return '';
|
||||
}, [alert]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-end">
|
||||
<div className={styles.alertRow}>
|
||||
<Group>
|
||||
{alert.state === AlertState.ALERT && (
|
||||
<div className="badge bg-danger">ALERT</div>
|
||||
)}
|
||||
{alert.state === AlertState.OK && (
|
||||
<div className="badge bg-success">OK</div>
|
||||
<Badge color="red" size="sm">
|
||||
Alert
|
||||
</Badge>
|
||||
)}
|
||||
{alert.state === AlertState.OK && <Badge size="sm">Ok</Badge>}
|
||||
{alert.state === AlertState.DISABLED && (
|
||||
<div className="badge bg-secondary">DISABLED</div>
|
||||
)}{' '}
|
||||
<Badge color="gray" size="sm">
|
||||
Disabled
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<Stack spacing={2}>
|
||||
<div>
|
||||
<Link href={alertUrl}>
|
||||
<a
|
||||
className={styles.alertLink}
|
||||
title={
|
||||
alert.source === 'CHART' ? 'Dashboard chart' : 'Saved search'
|
||||
}
|
||||
>
|
||||
<i
|
||||
className={`bi ${
|
||||
alert.source === 'CHART'
|
||||
? 'bi-graph-up'
|
||||
: 'bi-layout-text-sidebar-reverse'
|
||||
} text-slate-200 me-2 fs-8`}
|
||||
/>
|
||||
{alertName}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-slate-400 fs-8 d-flex gap-2">
|
||||
If {alert.source === 'LOG' ? 'count' : 'value'} is{' '}
|
||||
{alert.type === 'presence' ? 'over' : 'under'}{' '}
|
||||
<span className="fw-bold">{alert.threshold}</span>
|
||||
<span className="text-slate-400">·</span>
|
||||
{alert.channel.type === 'webhook' && (
|
||||
<span>Notify via Webhook</span>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
<AlertHistoryCardList history={alert.history} />
|
||||
{/* can we disable an alert that is alarming? hmmmmm */}
|
||||
{/* also, will make the alert jump from under the cursor to the disabled area */}
|
||||
{showDisableButton ? (
|
||||
<button
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
title="Disable/enable"
|
||||
{DISABLE_ALERTS_ENABLED ? (
|
||||
<Button
|
||||
size="xs"
|
||||
compact
|
||||
color="gray"
|
||||
onClick={() => {
|
||||
disableAlert(alert._id);
|
||||
}}
|
||||
>
|
||||
<i className="bi bi-gear"></i>
|
||||
</button>
|
||||
Disable
|
||||
</Button>
|
||||
) : null}
|
||||
<div className="fs-6 mt-2">
|
||||
{alert.channel.type === 'webhook' && (
|
||||
<span>
|
||||
Notifies via <span className="fw-bold">Webhook</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="fs-6 mt-2">
|
||||
Alerts if
|
||||
<span className="fw-bold">
|
||||
{' '}
|
||||
{alert.source === 'LOG' ? 'count' : 'value'}{' '}
|
||||
</span>
|
||||
is
|
||||
<span className="fw-bold">
|
||||
{' '}
|
||||
{alert.type === 'presence' ? 'over' : 'under'}{' '}
|
||||
</span>
|
||||
<span className="fw-bold">{alert.threshold}</span>
|
||||
{history.length > 0 && history[0]?.lastValues.length > 0 && (
|
||||
<span className="fw-light">
|
||||
{' '}
|
||||
(most recently{' '}
|
||||
{history[0].lastValues.map(({ count }) => count).join(', ')})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<AlertHistoryCardList history={history} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ChartAlertCard({ alert }: { alert: AlertData }) {
|
||||
const { history } = alert;
|
||||
if (!alert.dashboard) {
|
||||
throw new Error('alertData.dashboard is undefined');
|
||||
}
|
||||
return (
|
||||
<div className="bg-hdx-dark rounded p-3 d-flex align-items-center justify-content-between text-white-hover-success-trigger">
|
||||
<Link
|
||||
href={`/dashboards/${alert.dashboard._id}`}
|
||||
key={alert.dashboard._id}
|
||||
>
|
||||
{alert.dashboard.name}
|
||||
</Link>
|
||||
<AlertDetails alert={alert} history={history} />
|
||||
</Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LogAlertCard({ alert }: { alert: AlertData }) {
|
||||
const { history } = alert;
|
||||
if (!alert.logView) {
|
||||
throw new Error('alert.logView is undefined');
|
||||
}
|
||||
return (
|
||||
<div className="bg-hdx-dark rounded p-3 d-flex align-items-center justify-content-between text-white-hover-success-trigger">
|
||||
<Link href={`/search/${alert.logView._id}`} key={alert.logView._id}>
|
||||
{alert.logView?.name}
|
||||
</Link>
|
||||
<AlertDetails alert={alert} history={history} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertCard({ alert }: { alert: AlertData }) {
|
||||
if (alert.source === 'LOG') {
|
||||
return <LogAlertCard alert={alert} />;
|
||||
} else {
|
||||
return <ChartAlertCard alert={alert} />;
|
||||
}
|
||||
}
|
||||
|
||||
function AlertCardList({ alerts }: { alerts: AlertData[] }) {
|
||||
const alarmAlerts = alerts.filter(alert => alert.state === AlertState.ALERT);
|
||||
const okData = alerts.filter(alert => alert.state === AlertState.OK);
|
||||
|
|
@ -171,63 +204,84 @@ function AlertCardList({ alerts }: { alerts: AlertData[] }) {
|
|||
alert.state === AlertState.INSUFFICIENT_DATA,
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex flex-column gap-4">
|
||||
{alarmAlerts.length > 0 && (
|
||||
<div>
|
||||
<div className="fs-5 mb-3 text-danger">
|
||||
<i className="bi bi-exclamation-triangle"></i> Alarmed
|
||||
<div className={styles.sectionHeader}>
|
||||
<i className="bi bi-exclamation-triangle"></i> Triggered
|
||||
</div>
|
||||
{alarmAlerts.map((alert, index) => (
|
||||
<AlertCard key={index} alert={alert} />
|
||||
<AlertDetails key={index} alert={alert} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="fs-5 mb-3">
|
||||
<div className={styles.sectionHeader}>
|
||||
<i className="bi bi-repeat"></i> Running
|
||||
</div>
|
||||
{okData.length === 0 && (
|
||||
<div className="text-center text-muted">No alerts</div>
|
||||
<div className="text-center text-slate-400 my-4 fs-8">No alerts</div>
|
||||
)}
|
||||
{okData.map((alert, index) => (
|
||||
<AlertCard key={index} alert={alert} />
|
||||
<AlertDetails key={index} alert={alert} />
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<div className="fs-5 mb-3">
|
||||
<i className="bi bi-stop"></i> Disabled
|
||||
{DISABLE_ALERTS_ENABLED && (
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
<i className="bi bi-stop"></i> Disabled
|
||||
</div>
|
||||
{disabledData.length === 0 && (
|
||||
<div className="text-center text-slate-400 my-4 fs-8">
|
||||
No alerts
|
||||
</div>
|
||||
)}
|
||||
{disabledData.map((alert, index) => (
|
||||
<AlertDetails key={index} alert={alert} />
|
||||
))}
|
||||
</div>
|
||||
{disabledData.length === 0 && (
|
||||
<div className="text-center text-muted">No alerts</div>
|
||||
)}
|
||||
{disabledData.map((alert, index) => (
|
||||
<AlertCard key={index} alert={alert} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AlertsPage() {
|
||||
const alerts = api.useAlerts().data?.data;
|
||||
const { data, isError, isLoading } = api.useAlerts();
|
||||
const alerts = data?.data;
|
||||
|
||||
// TODO: Error and loading states
|
||||
|
||||
return (
|
||||
<div className="AlertsPage d-flex" style={{ height: '100vh' }}>
|
||||
<div className="AlertsPage" style={{ minHeight: '100vh' }}>
|
||||
<Head>
|
||||
<title>Alerts - HyperDX</title>
|
||||
</Head>
|
||||
<AppNav fixed />
|
||||
<div className="d-flex flex-column flex-grow-1 px-3 pt-3">
|
||||
<div className="d-flex justify-content-between">
|
||||
<div className="fs-4 mb-3">Alerts</div>
|
||||
</div>
|
||||
<div className="fw-light">
|
||||
Note that for now, you'll need to go to either the dashboard or
|
||||
saved search pages in order to create alerts. This is merely a place
|
||||
to enable/disable and get an overview of which alerts are in which
|
||||
state.
|
||||
</div>
|
||||
<div style={{ minHeight: 0 }} className="mt-4">
|
||||
<AlertCardList alerts={alerts || []} />
|
||||
<div className="d-flex">
|
||||
<AppNav fixed />
|
||||
<div className="w-100">
|
||||
<div className={styles.header}>Alerts</div>
|
||||
<div className="my-4">
|
||||
<Container>
|
||||
<MAlert
|
||||
icon={<i className="bi bi-info-circle-fill text-slate-400" />}
|
||||
color="gray"
|
||||
>
|
||||
Alerts can be created from dashboard charts and from saved
|
||||
searches.
|
||||
</MAlert>
|
||||
{isLoading ? (
|
||||
<div className="text-center text-slate-400 my-4 fs-8">
|
||||
Loading...
|
||||
</div>
|
||||
) : isError ? (
|
||||
<div className="text-center text-slate-400 my-4 fs-8">
|
||||
Error
|
||||
</div>
|
||||
) : (
|
||||
alerts?.length && <AlertCardList alerts={alerts} />
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
useState,
|
||||
} from 'react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import produce from 'immer';
|
||||
import { Button, Form, Modal } from 'react-bootstrap';
|
||||
|
|
@ -100,6 +101,7 @@ const Tile = forwardRef(
|
|||
onMouseUp,
|
||||
onTouchEnd,
|
||||
children,
|
||||
isHighlighed,
|
||||
}: {
|
||||
chart: Chart;
|
||||
alert?: Alert;
|
||||
|
|
@ -119,6 +121,7 @@ const Tile = forwardRef(
|
|||
onMouseUp?: (e: React.MouseEvent) => void;
|
||||
onTouchEnd?: (e: React.TouchEvent) => void;
|
||||
children?: React.ReactNode; // Resizer tooltip
|
||||
isHighlighed?: boolean;
|
||||
},
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
|
|
@ -189,9 +192,20 @@ const Tile = forwardRef(
|
|||
}
|
||||
}, [config.type, onSettled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isHighlighed) {
|
||||
document
|
||||
.getElementById(`chart-${chart.id}`)
|
||||
?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-hdx-dark p-3 ${className} d-flex flex-column`}
|
||||
className={`bg-hdx-dark p-3 ${className} d-flex flex-column ${
|
||||
isHighlighed && 'dashboard-chart-highlighted'
|
||||
}`}
|
||||
id={`chart-${chart.id}`}
|
||||
key={chart.id}
|
||||
ref={ref}
|
||||
style={style}
|
||||
|
|
@ -204,19 +218,20 @@ const Tile = forwardRef(
|
|||
<i className="bi bi-grip-horizontal text-muted" />
|
||||
<div className="fs-7 text-muted d-flex gap-2 align-items-center">
|
||||
{alert && (
|
||||
<div className="rounded px-1 text-muted bg-grey opacity-90 cursor-default">
|
||||
{alert.state === 'ALERT' ? (
|
||||
<Link href="/alerts">
|
||||
<div
|
||||
className={`rounded px-1 text-muted cursor-pointer ${
|
||||
alert.state === 'ALERT'
|
||||
? 'bg-danger effect-pulse'
|
||||
: 'bg-grey opacity-90'
|
||||
}`}
|
||||
>
|
||||
<i
|
||||
className="bi bi-bell text-danger effect-pulse"
|
||||
title="Has alert and is in ALERT state"
|
||||
></i>
|
||||
) : (
|
||||
<i
|
||||
className="bi bi-bell"
|
||||
title="Has alert and is in OK state"
|
||||
></i>
|
||||
)}
|
||||
</div>
|
||||
className="bi bi-bell text-white"
|
||||
title={`Has alert and is in ${alert.state} state`}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Button
|
||||
|
|
@ -732,6 +747,8 @@ export default function DashboardPage() {
|
|||
}
|
||||
}, [isLocalDashboard, router, dashboard?.charts.length]);
|
||||
|
||||
const [highlightedChartId] = useQueryParam('highlightedChartId');
|
||||
|
||||
const tiles = useMemo(
|
||||
() =>
|
||||
(dashboard?.charts ?? []).map(chart => {
|
||||
|
|
@ -744,6 +761,7 @@ export default function DashboardPage() {
|
|||
onEditClick={() => setEditedChart(chart)}
|
||||
granularity={granularityQuery}
|
||||
alert={dashboard?.alerts?.find(a => a.chartId === chart.id)}
|
||||
isHighlighed={highlightedChartId === chart.id}
|
||||
onDuplicateClick={async () => {
|
||||
if (dashboard != null) {
|
||||
if (!(await confirm(`Duplicate ${chart.name}?`, 'Duplicate'))) {
|
||||
|
|
@ -781,6 +799,7 @@ export default function DashboardPage() {
|
|||
dashboardQuery,
|
||||
searchedTimeRange,
|
||||
granularityQuery,
|
||||
highlightedChartId,
|
||||
confirm,
|
||||
setDashboard,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -406,11 +406,16 @@ export default function SearchPage() {
|
|||
onTimeRangeSelect,
|
||||
} = useTimeQuery({ isUTC });
|
||||
|
||||
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
||||
useEffect(() => {
|
||||
setIsFirstLoad(false);
|
||||
}, []);
|
||||
const [_searchedQuery, setSearchedQuery] = useQueryParam(
|
||||
'q',
|
||||
withDefault(StringParam, undefined),
|
||||
{
|
||||
updateType: 'pushIn',
|
||||
// prevent hijacking browser back button
|
||||
updateType: isFirstLoad ? 'replaceIn' : 'pushIn',
|
||||
// Workaround for qparams not being set properly: https://github.com/pbeshai/use-query-params/issues/233
|
||||
enableBatching: true,
|
||||
},
|
||||
|
|
|
|||
79
packages/app/styles/AlertsPage.module.scss
Normal file
79
packages/app/styles/AlertsPage.module.scss
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
@import './variables';
|
||||
|
||||
// TODO: Move to shared styles
|
||||
|
||||
.header {
|
||||
align-items: center;
|
||||
background-color: $body-bg;
|
||||
border-bottom: 1px solid $slate-950;
|
||||
color: $slate-200;
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
height: 60px;
|
||||
justify-content: space-between;
|
||||
line-height: 1;
|
||||
padding: 0 32px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
border-bottom: 1px solid $slate-950;
|
||||
margin-top: 30px;
|
||||
padding-bottom: 10px;
|
||||
font-size: 14px;
|
||||
color: $slate-400;
|
||||
}
|
||||
|
||||
.historyCardWrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
&:hover {
|
||||
.historyCard {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.historyCard {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: $slate-950;
|
||||
border-radius: 2px;
|
||||
margin: 0 1px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.1s ease-in;
|
||||
|
||||
&:hover {
|
||||
transition: none;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&.ok {
|
||||
background-color: #00d474;
|
||||
}
|
||||
|
||||
&.alarm {
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
}
|
||||
|
||||
.alertRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid $slate-950;
|
||||
}
|
||||
|
||||
.alertLink {
|
||||
font-weight: 500;
|
||||
color: $slate-200;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
|
@ -756,3 +756,16 @@ div.react-datepicker {
|
|||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-chart-highlighted {
|
||||
animation: highlightedChart 1.5s forwards;
|
||||
border: 1px solid rgba(255, 166, 0, 0.5);
|
||||
}
|
||||
|
||||
@keyframes highlightedChart {
|
||||
0% {
|
||||
background-color: rgba(255, 166, 0, 0.5);
|
||||
}
|
||||
100% {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue