mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-22 18:00:25 +01:00
Compare commits
1 Commits
feature/da
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2a18e8b6c |
@@ -11,5 +11,4 @@ export enum FeatureKeys {
|
||||
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
|
||||
USE_JSON_BODY = 'use_json_body',
|
||||
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
|
||||
DASHBOARD_V2 = 'dashboard_v2',
|
||||
}
|
||||
|
||||
@@ -42,5 +42,4 @@ export enum LOCALSTORAGE {
|
||||
LICENSE_KEY_CALLOUT_DISMISSED = 'LICENSE_KEY_CALLOUT_DISMISSED',
|
||||
DASHBOARD_PREFERENCES = 'DASHBOARD_PREFERENCES',
|
||||
ACTIVE_SIGNOZ_INSTANCE_URL = 'ACTIVE_SIGNOZ_INSTANCE_URL',
|
||||
DASHBOARDS_LIST_VISIBLE_COLUMNS = 'DASHBOARDS_LIST_VISIBLE_COLUMNS',
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
export function useIsDashboardV2(): boolean {
|
||||
const { featureFlags } = useAppContext();
|
||||
return Boolean(
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.DASHBOARD_V2)?.active,
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,15 @@
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { getBasePath } from 'utils/basePath';
|
||||
|
||||
export default createBrowserHistory({ basename: getBasePath() });
|
||||
const history = createBrowserHistory({ basename: getBasePath() });
|
||||
|
||||
let inAppPushCount = 0;
|
||||
history.listen((_, action) => {
|
||||
if (action === 'PUSH') {
|
||||
inAppPushCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
export const hasInAppHistory = (): boolean => inAppPushCount > 0;
|
||||
|
||||
export default history;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
function DashboardPageV2(): JSX.Element {
|
||||
return <>DashboardPageV2</>;
|
||||
}
|
||||
|
||||
export default DashboardPageV2;
|
||||
@@ -1,3 +1,8 @@
|
||||
import DashboardPageV2 from './DashboardPageV2';
|
||||
|
||||
function DashboardPageV2(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard Page V2</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default DashboardPageV2;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
gap: 8px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.headerLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--muted-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { AnnouncementBanner } from '@signozhq/ui/announcement-banner';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { LayoutGrid } from '@signozhq/icons';
|
||||
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import DashboardsList from './components/DashboardsList';
|
||||
|
||||
import styles from './DashboardsListPageV2.module.scss';
|
||||
|
||||
function DashboardsListPageV2(): JSX.Element {
|
||||
const [showBanner, setShowBanner] = useState(true);
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
{showBanner && (
|
||||
<AnnouncementBanner
|
||||
type="warning"
|
||||
onClose={(): void => setShowBanner(false)}
|
||||
>
|
||||
You're on the V2 dashboards page. If you landed here unintentionally,
|
||||
please reach out to Ashwin.
|
||||
</AnnouncementBanner>
|
||||
)}
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerLeft}>
|
||||
<LayoutGrid size={14} className={styles.icon} />
|
||||
<Typography.Text className={styles.text}>Dashboards</Typography.Text>
|
||||
</div>
|
||||
<HeaderRightSection
|
||||
enableAnnouncements={false}
|
||||
enableShare
|
||||
enableFeedback
|
||||
/>
|
||||
</div>
|
||||
<DashboardsList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardsListPageV2;
|
||||
@@ -1,44 +0,0 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.actionButton:hover {
|
||||
background: color-mix(in srgb, var(--card) 60%, transparent);
|
||||
}
|
||||
|
||||
.triggerIcon {
|
||||
margin-left: 16px;
|
||||
flex: 0 0 auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.dashboardActionsPopover) {
|
||||
:global(.ant-popover-inner) {
|
||||
width: 187px;
|
||||
height: auto;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import { Button, Popover } from 'antd';
|
||||
import {
|
||||
Expand,
|
||||
EllipsisVertical,
|
||||
Link2,
|
||||
SquareArrowOutUpRight,
|
||||
} from '@signozhq/icons';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import styles from './ActionsPopover.module.scss';
|
||||
|
||||
interface Props {
|
||||
link: string;
|
||||
dashboardId: string;
|
||||
dashboardName: string;
|
||||
createdBy: string;
|
||||
isLocked: boolean;
|
||||
onView: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
function ActionsPopover({
|
||||
link,
|
||||
dashboardId,
|
||||
dashboardName,
|
||||
createdBy,
|
||||
isLocked,
|
||||
onView,
|
||||
}: Props): JSX.Element {
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
<div className={styles.content}>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.actionButton}
|
||||
icon={<Expand size={12} />}
|
||||
onClick={onView}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.actionButton}
|
||||
icon={<SquareArrowOutUpRight size={12} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
openInNewTab(link);
|
||||
}}
|
||||
>
|
||||
Open in New Tab
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.actionButton}
|
||||
icon={<Link2 size={12} />}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setCopy(getAbsoluteUrl(link));
|
||||
}}
|
||||
>
|
||||
Copy Link
|
||||
</Button>
|
||||
<DeleteButton
|
||||
name={dashboardName}
|
||||
id={dashboardId}
|
||||
isLocked={isLocked}
|
||||
createdBy={createdBy}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
placement="bottomRight"
|
||||
arrow={false}
|
||||
rootClassName="dashboardActionsPopover"
|
||||
trigger="click"
|
||||
>
|
||||
<EllipsisVertical
|
||||
className={styles.triggerIcon}
|
||||
size={14}
|
||||
data-testid="dashboard-action-icon"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionsPopover;
|
||||
@@ -1,160 +0,0 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
display: flex;
|
||||
padding: 12px 14.634px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 7.317px;
|
||||
border-radius: 4px;
|
||||
border: 0.915px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.previewHeader {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.previewIcon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.previewTitle {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12.805px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18.293px;
|
||||
letter-spacing: -0.064px;
|
||||
}
|
||||
|
||||
.previewDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.previewRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.formattedTime {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12.805px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16.463px;
|
||||
letter-spacing: -0.064px;
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.userTag {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--l2-foreground);
|
||||
font-size: 8px;
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: normal;
|
||||
letter-spacing: -0.05px;
|
||||
border-radius: 12.805px;
|
||||
background-color: var(--l1-background);
|
||||
}
|
||||
|
||||
.userLabel {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12.805px;
|
||||
font-weight: 400;
|
||||
line-height: 16.463px;
|
||||
letter-spacing: -0.064px;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0px 0px 0px 14.634px;
|
||||
}
|
||||
|
||||
.actionLeft {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.connectionLine {
|
||||
border-top: 1px dashed var(--l1-border);
|
||||
min-width: 20px;
|
||||
flex-grow: 1;
|
||||
margin: 0px 8px;
|
||||
}
|
||||
|
||||
.actionRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.saveChanges {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 8px 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-border);
|
||||
}
|
||||
|
||||
:global(.configureMetadataModalRoot) {
|
||||
:global(.ant-modal-content) {
|
||||
width: 500px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--card);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
:global(.ant-modal-header) {
|
||||
background: var(--card);
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
:global(.ant-modal-body) {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
:global(.ant-modal-footer) {
|
||||
margin-top: 0px;
|
||||
padding: 4px 16px 16px 16px;
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Modal, Switch } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { CalendarClock, Check, Clock4 } from '@signozhq/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { get, isEmpty } from 'lodash-es';
|
||||
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import type { DashboardListItem } from '../../utils';
|
||||
import {
|
||||
DynamicColumns,
|
||||
useDashboardsListVisibleColumnsStore,
|
||||
type DashboardDynamicColumns,
|
||||
} from './useDynamicColumns';
|
||||
|
||||
import styles from './ConfigureMetadataModal.module.scss';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
previewDashboard: DashboardListItem | undefined;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function lastUpdatedLabel(time: string | undefined): string {
|
||||
if (!time || isEmpty(time)) {
|
||||
return 'No updates yet!';
|
||||
}
|
||||
const diff = dayjs();
|
||||
const ref = dayjs(time);
|
||||
const months = diff.diff(ref, 'months');
|
||||
if (months > 0) {
|
||||
return `Last Updated ${months} months ago`;
|
||||
}
|
||||
const days = diff.diff(ref, 'days');
|
||||
if (days > 0) {
|
||||
return `Last Updated ${days} days ago`;
|
||||
}
|
||||
const hours = diff.diff(ref, 'hours');
|
||||
if (hours > 0) {
|
||||
return `Last Updated ${hours} hrs ago`;
|
||||
}
|
||||
const minutes = diff.diff(ref, 'minutes');
|
||||
if (minutes > 0) {
|
||||
return `Last Updated ${minutes} mins ago`;
|
||||
}
|
||||
const seconds = diff.diff(ref, 'seconds');
|
||||
return `Last Updated ${seconds} sec ago`;
|
||||
}
|
||||
|
||||
function ConfigureMetadataModal({
|
||||
open,
|
||||
previewDashboard,
|
||||
onClose,
|
||||
}: Props): JSX.Element {
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const storedColumns = useDashboardsListVisibleColumnsStore(
|
||||
(s) => s.visibleColumns,
|
||||
);
|
||||
const setStoredColumns = useDashboardsListVisibleColumnsStore(
|
||||
(s) => s.setVisibleColumns,
|
||||
);
|
||||
const [draftColumns, setDraftColumns] =
|
||||
useState<DashboardDynamicColumns>(storedColumns);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setDraftColumns(storedColumns);
|
||||
}
|
||||
}, [open, storedColumns]);
|
||||
|
||||
const handleSave = (): void => {
|
||||
setStoredColumns(draftColumns);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const previewImage = previewDashboard?.data?.metadata?.image || Base64Icons[0];
|
||||
const previewName = previewDashboard?.data?.spec?.display?.name;
|
||||
const previewCreatedBy = previewDashboard?.createdBy;
|
||||
const previewUpdatedBy = previewDashboard?.updatedBy;
|
||||
const previewUpdatedAt = previewDashboard?.updatedAt;
|
||||
|
||||
const formattedCreatedAt = previewDashboard
|
||||
? formatTimezoneAdjustedTimestamp(
|
||||
get(previewDashboard, 'createdAt', '') as string,
|
||||
DATE_TIME_FORMATS.DASH_DATETIME_UTC,
|
||||
)
|
||||
: '';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
title="Configure Metadata"
|
||||
footer={
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Check size={14} />}
|
||||
className={styles.saveChanges}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
}
|
||||
rootClassName="configureMetadataModalRoot"
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.preview}>
|
||||
<section className={styles.previewHeader}>
|
||||
<img
|
||||
src={previewImage}
|
||||
alt="dashboard-image"
|
||||
className={styles.previewIcon}
|
||||
/>
|
||||
<Typography.Text className={styles.previewTitle}>
|
||||
{previewName}
|
||||
</Typography.Text>
|
||||
</section>
|
||||
<section className={styles.previewDetails}>
|
||||
<section className={styles.previewRow}>
|
||||
{draftColumns.createdAt && (
|
||||
<Typography.Text className={styles.formattedTime}>
|
||||
<CalendarClock size={14} />
|
||||
{formattedCreatedAt}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{draftColumns.createdBy && (
|
||||
<div className={styles.user}>
|
||||
<Typography.Text className={styles.userTag}>
|
||||
{previewCreatedBy?.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.userLabel}>
|
||||
{previewCreatedBy}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<section className={styles.previewRow}>
|
||||
{draftColumns.updatedAt && (
|
||||
<Typography.Text className={styles.formattedTime}>
|
||||
<CalendarClock size={14} />
|
||||
{lastUpdatedLabel(previewUpdatedAt)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{draftColumns.updatedBy && (
|
||||
<div className={styles.user}>
|
||||
<Typography.Text className={styles.userTag}>
|
||||
{previewUpdatedBy?.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.userLabel}>
|
||||
{previewUpdatedBy}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className={styles.action}>
|
||||
<div className={styles.actionLeft}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>Created at</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.connectionLine} />
|
||||
<div className={styles.actionRight}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked
|
||||
disabled
|
||||
onChange={(check): void =>
|
||||
setDraftColumns((prev) => ({
|
||||
...prev,
|
||||
[DynamicColumns.CREATED_AT]: check,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.action}>
|
||||
<div className={styles.actionLeft}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>Created by</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.connectionLine} />
|
||||
<div className={styles.actionRight}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked
|
||||
disabled
|
||||
onChange={(check): void =>
|
||||
setDraftColumns((prev) => ({
|
||||
...prev,
|
||||
[DynamicColumns.CREATED_BY]: check,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.action}>
|
||||
<div className={styles.actionLeft}>
|
||||
<Clock4 size={14} />
|
||||
<Typography.Text>Updated at</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.connectionLine} />
|
||||
<div className={styles.actionRight}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={draftColumns.updatedAt}
|
||||
onChange={(check): void =>
|
||||
setDraftColumns((prev) => ({
|
||||
...prev,
|
||||
[DynamicColumns.UPDATED_AT]: check,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.action}>
|
||||
<div className={styles.actionLeft}>
|
||||
<Clock4 size={14} />
|
||||
<Typography.Text>Updated by</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.connectionLine} />
|
||||
<div className={styles.actionRight}>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={draftColumns.updatedBy}
|
||||
onChange={(check): void =>
|
||||
setDraftColumns((prev) => ({
|
||||
...prev,
|
||||
[DynamicColumns.UPDATED_BY]: check,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfigureMetadataModal;
|
||||
@@ -1,52 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
export interface DashboardDynamicColumns {
|
||||
createdAt: boolean;
|
||||
createdBy: boolean;
|
||||
updatedAt: boolean;
|
||||
updatedBy: boolean;
|
||||
}
|
||||
|
||||
export enum DynamicColumns {
|
||||
CREATED_AT = 'createdAt',
|
||||
CREATED_BY = 'createdBy',
|
||||
UPDATED_AT = 'updatedAt',
|
||||
UPDATED_BY = 'updatedBy',
|
||||
}
|
||||
|
||||
const DEFAULT_COLUMNS: DashboardDynamicColumns = {
|
||||
createdAt: true,
|
||||
createdBy: true,
|
||||
updatedAt: false,
|
||||
updatedBy: false,
|
||||
};
|
||||
|
||||
interface DashboardsListVisibleColumnsState {
|
||||
visibleColumns: DashboardDynamicColumns;
|
||||
setVisibleColumns: (next: DashboardDynamicColumns) => void;
|
||||
}
|
||||
|
||||
export const useDashboardsListVisibleColumnsStore =
|
||||
create<DashboardsListVisibleColumnsState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
visibleColumns: DEFAULT_COLUMNS,
|
||||
setVisibleColumns: (next): void => {
|
||||
set({ visibleColumns: next });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: LOCALSTORAGE.DASHBOARDS_LIST_VISIBLE_COLUMNS,
|
||||
merge: (persisted, current) => ({
|
||||
...current,
|
||||
visibleColumns: {
|
||||
...DEFAULT_COLUMNS,
|
||||
...((persisted as Partial<DashboardsListVisibleColumnsState>)
|
||||
?.visibleColumns ?? {}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -1,34 +0,0 @@
|
||||
.menuItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.templatesItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.primaryButton {
|
||||
padding: 5.937px 11.875px;
|
||||
}
|
||||
|
||||
.textButton {
|
||||
display: flex;
|
||||
width: 153px;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 5.937px 11.875px;
|
||||
justify-content: center;
|
||||
gap: 5.937px;
|
||||
border-radius: 1.484px;
|
||||
background: var(--primary-background);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
:global(.createDashboardMenuOverlay) {
|
||||
width: 200px;
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
ExternalLink,
|
||||
Github,
|
||||
LayoutGrid,
|
||||
Plus,
|
||||
Radius,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import styles from './CreateDashboardDropdown.module.scss';
|
||||
|
||||
interface Props {
|
||||
canCreate: boolean;
|
||||
onCreate: () => void;
|
||||
onImportJSON: () => void;
|
||||
variant?: 'primary' | 'text';
|
||||
}
|
||||
|
||||
const TEMPLATES_HREF =
|
||||
'https://signoz.io/docs/dashboards/dashboard-templates/overview/';
|
||||
|
||||
function CreateDashboardDropdown({
|
||||
canCreate,
|
||||
onCreate,
|
||||
onImportJSON,
|
||||
variant = 'primary',
|
||||
}: Props): JSX.Element {
|
||||
const items: MenuProps['items'] = useMemo(() => {
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'import-json',
|
||||
label: (
|
||||
<div
|
||||
className={styles.menuItem}
|
||||
data-testid="import-json-menu-cta"
|
||||
onClick={onImportJSON}
|
||||
>
|
||||
<Radius size={14} /> Import JSON
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'view-templates',
|
||||
label: (
|
||||
<a
|
||||
href={TEMPLATES_HREF}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid="view-templates-menu-cta"
|
||||
>
|
||||
<div className={styles.templatesItem}>
|
||||
<div className={styles.menuItem}>
|
||||
<Github size={14} /> View templates
|
||||
</div>
|
||||
<ExternalLink size={14} />
|
||||
</div>
|
||||
</a>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (canCreate) {
|
||||
menuItems.unshift({
|
||||
key: 'create-dashboard',
|
||||
label: (
|
||||
<div
|
||||
className={styles.menuItem}
|
||||
data-testid="create-dashboard-menu-cta"
|
||||
onClick={onCreate}
|
||||
>
|
||||
<LayoutGrid size={14} /> Create dashboard
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
}, [canCreate, onCreate, onImportJSON]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
overlayClassName="createDashboardMenuOverlay"
|
||||
menu={{ items }}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
>
|
||||
{variant === 'primary' ? (
|
||||
<Button
|
||||
type="primary"
|
||||
className={cx('periscope-btn primary', styles.primaryButton)}
|
||||
icon={<Plus size={14} />}
|
||||
data-testid="new-dashboard-cta"
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List: New dashboard clicked', {});
|
||||
}}
|
||||
>
|
||||
New dashboard
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.textButton}
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List: New dashboard clicked', {});
|
||||
}}
|
||||
>
|
||||
New Dashboard
|
||||
</Button>
|
||||
)}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateDashboardDropdown;
|
||||
@@ -1,152 +0,0 @@
|
||||
.row {
|
||||
padding: 12px 16px 16px 16px;
|
||||
border: 1px solid var(--l1-border);
|
||||
border-top: none;
|
||||
background: var(--l2-background);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.titleWithAction {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.titleBlock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
line-height: 20px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.titleLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
line-height: 20px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tagsWithActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: flex;
|
||||
padding: 4px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 28px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
|
||||
color: var(--bg-sienna-400);
|
||||
text-align: center;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
margin-inline-end: 0px;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 24px;
|
||||
}
|
||||
|
||||
.createdAt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.createdBy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50px;
|
||||
background: var(--l1-border);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatarText {
|
||||
color: var(--l2-foreground);
|
||||
font-size: 8px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: normal;
|
||||
letter-spacing: -0.05px;
|
||||
}
|
||||
|
||||
.byLabel {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.updatedBy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
:global(.titleTooltipOverlay) {
|
||||
:global(.ant-tooltip-content) :global(.ant-tooltip-inner) {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
import { Tag, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { CalendarClock } from '@signozhq/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import dayjs from 'dayjs';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import type { DashboardListItem } from '../../utils';
|
||||
import { tagsToStrings } from '../../utils';
|
||||
import ActionsPopover from '../ActionsPopover/ActionsPopover';
|
||||
|
||||
import styles from './DashboardRow.module.scss';
|
||||
|
||||
interface Props {
|
||||
dashboard: DashboardListItem;
|
||||
index: number;
|
||||
canAct: boolean;
|
||||
showUpdatedAt: boolean;
|
||||
showUpdatedBy: boolean;
|
||||
}
|
||||
|
||||
function lastUpdatedLabel(time: string | undefined): string {
|
||||
if (!time || isEmpty(time)) {
|
||||
return 'No updates yet!';
|
||||
}
|
||||
const diff = dayjs();
|
||||
const ref = dayjs(time);
|
||||
const months = diff.diff(ref, 'months');
|
||||
if (months > 0) {
|
||||
return `Last Updated ${months} months ago`;
|
||||
}
|
||||
const days = diff.diff(ref, 'days');
|
||||
if (days > 0) {
|
||||
return `Last Updated ${days} days ago`;
|
||||
}
|
||||
const hours = diff.diff(ref, 'hours');
|
||||
if (hours > 0) {
|
||||
return `Last Updated ${hours} hrs ago`;
|
||||
}
|
||||
const minutes = diff.diff(ref, 'minutes');
|
||||
if (minutes > 0) {
|
||||
return `Last Updated ${minutes} mins ago`;
|
||||
}
|
||||
const seconds = diff.diff(ref, 'seconds');
|
||||
return `Last Updated ${seconds} sec ago`;
|
||||
}
|
||||
|
||||
function DashboardRow({
|
||||
dashboard,
|
||||
index,
|
||||
canAct,
|
||||
showUpdatedAt,
|
||||
showUpdatedBy,
|
||||
}: Props): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||
|
||||
const id = dashboard.id;
|
||||
const name = dashboard.data?.spec?.display?.name ?? '';
|
||||
const image = dashboard.data?.metadata?.image || Base64Icons[0];
|
||||
const createdBy = dashboard.createdBy ?? '';
|
||||
const updatedBy = dashboard.updatedBy ?? '';
|
||||
const createdAt = dashboard.createdAt ?? '';
|
||||
const updatedAt = dashboard.updatedAt ?? '';
|
||||
const isLocked = !!dashboard.locked;
|
||||
const tags = tagsToStrings(dashboard.data?.metadata?.tags);
|
||||
|
||||
const link = generatePath(ROUTES.DASHBOARD, { dashboardId: id });
|
||||
const formattedCreatedAt = formatTimezoneAdjustedTimestamp(
|
||||
createdAt,
|
||||
DATE_TIME_FORMATS.DASH_DATETIME_UTC,
|
||||
);
|
||||
|
||||
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
event.stopPropagation();
|
||||
safeNavigate(link, { newTab: isModifierKeyPressed(event) });
|
||||
logEvent('Dashboard List: Clicked on dashboard', {
|
||||
dashboardId: id,
|
||||
dashboardName: name,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.row} onClick={onClickHandler}>
|
||||
<div className={styles.titleWithAction}>
|
||||
<div className={styles.titleBlock}>
|
||||
<Tooltip
|
||||
title={name.length > 50 ? name : ''}
|
||||
placement="left"
|
||||
overlayClassName="titleTooltipOverlay"
|
||||
>
|
||||
<div className={styles.titleLink} onClick={onClickHandler}>
|
||||
<img src={image} alt="dashboard-image" className={styles.icon} />
|
||||
<Typography.Text
|
||||
data-testid={`dashboard-title-${index}`}
|
||||
className={styles.title}
|
||||
>
|
||||
{name}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className={styles.tagsWithActions}>
|
||||
{tags.length > 0 && (
|
||||
<div className={styles.tags}>
|
||||
{tags.slice(0, 3).map((tag) => (
|
||||
<Tag className={styles.tag} key={tag}>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
{tags.length > 3 && (
|
||||
<Tag className={styles.tag} key={tags[3]}>
|
||||
+ <span> {tags.length - 3} </span>
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{canAct && (
|
||||
<ActionsPopover
|
||||
link={link}
|
||||
dashboardId={id}
|
||||
dashboardName={name}
|
||||
createdBy={createdBy}
|
||||
isLocked={isLocked}
|
||||
onView={onClickHandler}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.createdAt}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>{formattedCreatedAt}</Typography.Text>
|
||||
</div>
|
||||
|
||||
{createdBy && (
|
||||
<div className={styles.createdBy}>
|
||||
<div className={styles.avatar}>
|
||||
<Typography.Text className={styles.avatarText}>
|
||||
{createdBy.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className={styles.byLabel}>{createdBy}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showUpdatedAt && (
|
||||
<div className={styles.createdAt}>
|
||||
<CalendarClock size={14} />
|
||||
<Typography.Text>{lastUpdatedLabel(updatedAt)}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updatedBy && showUpdatedBy && (
|
||||
<div className={styles.updatedBy}>
|
||||
<Typography.Text className={styles.byLabel}>
|
||||
Last Updated By -
|
||||
</Typography.Text>
|
||||
<div className={styles.avatar}>
|
||||
<Typography.Text className={styles.avatarText}>
|
||||
{updatedBy.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className={styles.byLabel}>{updatedBy}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardRow;
|
||||
@@ -1,96 +0,0 @@
|
||||
.container {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.viewContent {
|
||||
width: calc(100% - 30px);
|
||||
max-width: 836px;
|
||||
|
||||
:global(.ant-table-wrapper) :global(.ant-table-cell) {
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
background: var(--l1-background) !important;
|
||||
}
|
||||
|
||||
:global(.ant-table-wrapper)
|
||||
:global(.ant-table-tbody)
|
||||
:global(.ant-table-row)
|
||||
:global(.ant-table-cell)
|
||||
> div {
|
||||
// Row content is the only child of the td; it carries the borders.
|
||||
}
|
||||
|
||||
:global(.ant-table-wrapper)
|
||||
:global(.ant-table-tbody)
|
||||
:global(.ant-table-row:last-child)
|
||||
:global(.ant-table-cell)
|
||||
> div {
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item) > a {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item-active) {
|
||||
background-color: var(--primary-background);
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item-active) > a {
|
||||
color: var(--foreground) !important;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-lg);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 28px;
|
||||
letter-spacing: -0.09px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.integrationsContainer {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.integrationsContent {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { AxiosError } from 'axios';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
createDashboardV2,
|
||||
useListDashboardsV2,
|
||||
} from 'api/generated/services/dashboard';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { RequestDashboardBtn } from 'container/ListOfDashboard/RequestDashboardBtn';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import {
|
||||
usePage,
|
||||
useSearch,
|
||||
useSortColumn,
|
||||
useSortOrder,
|
||||
type SortColumn,
|
||||
type SortOrder,
|
||||
} from '../../hooks/useDashboardsListQueryParams';
|
||||
import { buildSearchDSL, type DashboardListItem } from '../../utils';
|
||||
import ConfigureMetadataModal from '../ConfigureMetadataModal/ConfigureMetadataModal';
|
||||
import { useDashboardsListVisibleColumnsStore } from '../ConfigureMetadataModal/useDynamicColumns';
|
||||
import CreateDashboardDropdown from '../CreateDashboardDropdown/CreateDashboardDropdown';
|
||||
import EmptyState from '../EmptyState/EmptyState';
|
||||
import ErrorState from '../ErrorState/ErrorState';
|
||||
import ImportJSONModal from '../ImportJSONModal/ImportJSONModal';
|
||||
import ListHeader from '../ListHeader/ListHeader';
|
||||
import LoadingState from '../LoadingState/LoadingState';
|
||||
import NoResultsState from '../NoResultsState/NoResultsState';
|
||||
import SearchBar from '../SearchBar/SearchBar';
|
||||
import DashboardsListContent from './DashboardsListContent';
|
||||
|
||||
import styles from './DashboardsList.module.scss';
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
function DashboardsList(): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { t } = useTranslation('dashboard');
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { isCloudUser } = useGetTenantLicense();
|
||||
|
||||
const { user } = useAppContext();
|
||||
const [action, canCreateNewDashboard] = useComponentPermission(
|
||||
['action', 'create_new_dashboards'],
|
||||
user.role,
|
||||
);
|
||||
|
||||
const [searchString, setSearchString] = useSearch();
|
||||
const [sortColumn, setSortColumn] = useSortColumn();
|
||||
const [sortOrder, setSortOrder] = useSortOrder();
|
||||
const [page, setPage] = usePage();
|
||||
|
||||
const [searchInput, setSearchInput] = useState(searchString);
|
||||
const debouncedSearch = useDebounce(searchInput, 300);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearch === searchString) {
|
||||
return;
|
||||
}
|
||||
setSearchString(debouncedSearch);
|
||||
setPage(1);
|
||||
}, [debouncedSearch, searchString, setSearchString, setPage]);
|
||||
|
||||
const listParams = useMemo(
|
||||
() => ({
|
||||
query: buildSearchDSL(searchString),
|
||||
sort: sortColumn,
|
||||
order: sortOrder,
|
||||
limit: PAGE_SIZE,
|
||||
offset: (page - 1) * PAGE_SIZE,
|
||||
}),
|
||||
[searchString, sortColumn, sortOrder, page],
|
||||
);
|
||||
|
||||
const {
|
||||
data: response,
|
||||
isLoading,
|
||||
isFetching,
|
||||
error,
|
||||
refetch,
|
||||
} = useListDashboardsV2(listParams, { query: { keepPreviousData: true } });
|
||||
|
||||
const dashboards = useMemo<DashboardListItem[]>(
|
||||
() => response?.data?.dashboards ?? [],
|
||||
[response],
|
||||
);
|
||||
const total = response?.data?.total ?? 0;
|
||||
|
||||
const [isImportOpen, setIsImportOpen] = useState(false);
|
||||
const [isConfigureOpen, setIsConfigureOpen] = useState(false);
|
||||
const visibleColumns = useDashboardsListVisibleColumnsStore(
|
||||
(s) => s.visibleColumns,
|
||||
);
|
||||
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const handleCreateNew = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
logEvent('Dashboard List: Create dashboard clicked', {});
|
||||
setCreating(true);
|
||||
const created = await createDashboardV2({
|
||||
metadata: { schemaVersion: 'v6' },
|
||||
spec: {
|
||||
display: { name: t('new_dashboard_title', { ns: 'dashboard' }) },
|
||||
},
|
||||
});
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, { dashboardId: created.data.id }),
|
||||
);
|
||||
} catch (e) {
|
||||
showErrorModal(e as APIError);
|
||||
notifications.error({
|
||||
message: (e as AxiosError).toString() || 'Failed to create dashboard',
|
||||
});
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
}, [safeNavigate, showErrorModal, notifications, t]);
|
||||
|
||||
const handleImportToggle = useCallback((): void => {
|
||||
logEvent('Dashboard List V2: Import JSON clicked', {});
|
||||
setIsImportOpen((s) => !s);
|
||||
}, []);
|
||||
|
||||
const onSortChange = useCallback(
|
||||
(column: SortColumn): void => {
|
||||
setSortColumn(column);
|
||||
setPage(1);
|
||||
},
|
||||
[setSortColumn, setPage],
|
||||
);
|
||||
|
||||
const onOrderChange = useCallback(
|
||||
(order: SortOrder): void => {
|
||||
setSortOrder(order);
|
||||
setPage(1);
|
||||
},
|
||||
[setSortOrder, setPage],
|
||||
);
|
||||
|
||||
const visitLoggedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!visitLoggedRef.current && !isLoading && response !== undefined) {
|
||||
logEvent('Dashboard List V2: Page visited', { number: dashboards.length });
|
||||
visitLoggedRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.viewContent}>
|
||||
<div className={styles.titleContainer}>
|
||||
<Typography.Title className={styles.title}>Dashboards</Typography.Title>
|
||||
<Typography.Text className={styles.subtitle}>
|
||||
Create and manage dashboards for your workspace.
|
||||
</Typography.Text>
|
||||
{isCloudUser && (
|
||||
<div className={styles.integrationsContainer}>
|
||||
<div className={styles.integrationsContent}>
|
||||
<RequestDashboardBtn />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<LoadingState />
|
||||
) : error ? (
|
||||
<ErrorState
|
||||
isCloudUser={!!isCloudUser}
|
||||
onRetry={(): void => {
|
||||
refetch();
|
||||
}}
|
||||
/>
|
||||
) : dashboards.length === 0 && !searchString && page === 1 ? (
|
||||
<EmptyState
|
||||
createDropdown={
|
||||
canCreateNewDashboard ? (
|
||||
<CreateDashboardDropdown
|
||||
canCreate={!!canCreateNewDashboard}
|
||||
onCreate={handleCreateNew}
|
||||
onImportJSON={handleImportToggle}
|
||||
variant="text"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.toolbar}>
|
||||
<SearchBar value={searchInput} onChange={setSearchInput} />
|
||||
{canCreateNewDashboard && (
|
||||
<CreateDashboardDropdown
|
||||
canCreate={!!canCreateNewDashboard}
|
||||
onCreate={handleCreateNew}
|
||||
onImportJSON={handleImportToggle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{dashboards.length === 0 ? (
|
||||
<NoResultsState searchString={searchInput} />
|
||||
) : (
|
||||
<>
|
||||
<ListHeader
|
||||
sortColumn={sortColumn}
|
||||
onSortChange={onSortChange}
|
||||
sortOrder={sortOrder}
|
||||
onOrderChange={onOrderChange}
|
||||
onConfigureMetadata={(): void => setIsConfigureOpen(true)}
|
||||
/>
|
||||
<DashboardsListContent
|
||||
dashboards={dashboards}
|
||||
page={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
total={total}
|
||||
onPageChange={setPage}
|
||||
canAct={!!action}
|
||||
showUpdatedAt={visibleColumns.updatedAt}
|
||||
showUpdatedBy={visibleColumns.updatedBy}
|
||||
loading={creating || isFetching}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<ImportJSONModal
|
||||
open={isImportOpen}
|
||||
onClose={(): void => setIsImportOpen(false)}
|
||||
/>
|
||||
|
||||
<ConfigureMetadataModal
|
||||
open={isConfigureOpen}
|
||||
previewDashboard={dashboards[0]}
|
||||
onClose={(): void => setIsConfigureOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardsList;
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Table } from 'antd';
|
||||
import type { TableProps } from 'antd/lib';
|
||||
|
||||
import type { DashboardListItem } from '../../utils';
|
||||
import DashboardRow from '../DashboardRow/DashboardRow';
|
||||
|
||||
interface Props {
|
||||
dashboards: DashboardListItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
onPageChange: (page: number) => void;
|
||||
canAct: boolean;
|
||||
showUpdatedAt: boolean;
|
||||
showUpdatedBy: boolean;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
function DashboardsListContent({
|
||||
dashboards,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
onPageChange,
|
||||
canAct,
|
||||
showUpdatedAt,
|
||||
showUpdatedBy,
|
||||
loading,
|
||||
}: Props): JSX.Element {
|
||||
const columns: TableProps<DashboardListItem>['columns'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'Dashboards',
|
||||
key: 'dashboard',
|
||||
render: (_, dashboard, index): JSX.Element => (
|
||||
<DashboardRow
|
||||
dashboard={dashboard}
|
||||
index={index}
|
||||
canAct={canAct}
|
||||
showUpdatedAt={showUpdatedAt}
|
||||
showUpdatedBy={showUpdatedBy}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[canAct, showUpdatedAt, showUpdatedBy],
|
||||
);
|
||||
|
||||
const paginationConfig = total > pageSize && {
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
onChange: onPageChange,
|
||||
current: page,
|
||||
total,
|
||||
hideOnSinglePage: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dashboards.map((d) => ({ ...d, key: d.id }))}
|
||||
showSorterTooltip
|
||||
loading={loading}
|
||||
showHeader={false}
|
||||
pagination={paginationConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardsListContent;
|
||||
@@ -1,3 +0,0 @@
|
||||
import DashboardsList from './DashboardsList';
|
||||
|
||||
export default DashboardsList;
|
||||
@@ -1,64 +0,0 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 320px;
|
||||
padding: 105px 141px;
|
||||
margin-top: 16px;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--l1-border);
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.copy {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.noDashboard {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.learnMore {
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.learnMoreArrow {
|
||||
margin-left: -20px;
|
||||
color: var(--bg-robin-400);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ArrowUpRight } from '@signozhq/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import dashboardsUrl from '@/assets/Icons/dashboards.svg';
|
||||
|
||||
import styles from './EmptyState.module.scss';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
interface Props {
|
||||
createDropdown?: ReactNode;
|
||||
}
|
||||
|
||||
const LEARN_MORE_HREF =
|
||||
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state';
|
||||
|
||||
function EmptyState({ createDropdown }: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<img src={dashboardsUrl} alt="dashboards" className={styles.image} />
|
||||
<section className={styles.copy}>
|
||||
<Typography.Text className={styles.noDashboard}>
|
||||
No dashboards yet.{' '}
|
||||
</Typography.Text>
|
||||
<Typography.Text className={styles.info}>
|
||||
Create a dashboard to start visualizing your data
|
||||
</Typography.Text>
|
||||
</section>
|
||||
|
||||
{createDropdown ? (
|
||||
<section className={styles.actions}>
|
||||
{createDropdown}
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.learnMore}
|
||||
data-testid="learn-more"
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List: Learn more clicked', {});
|
||||
openInNewTab(LEARN_MORE_HREF);
|
||||
}}
|
||||
>
|
||||
Learn more
|
||||
</Button>
|
||||
<ArrowUpRight size={16} className={styles.learnMoreArrow} />
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmptyState;
|
||||
@@ -1,62 +0,0 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 320px;
|
||||
padding: 105px 141px;
|
||||
margin-top: 16px;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--l1-border);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.retryButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
padding: 5.937px 11.875px;
|
||||
justify-content: center;
|
||||
gap: 5.937px;
|
||||
border-radius: 1.484px;
|
||||
background: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.learnMore {
|
||||
color: var(--bg-robin-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.learnMoreArrow {
|
||||
margin-left: -20px;
|
||||
color: var(--bg-robin-400);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Button } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ArrowUpRight, RotateCw } from '@signozhq/icons';
|
||||
import { handleContactSupport } from 'container/Integrations/utils';
|
||||
|
||||
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
|
||||
|
||||
import styles from './ErrorState.module.scss';
|
||||
|
||||
interface Props {
|
||||
isCloudUser: boolean;
|
||||
onRetry: () => void;
|
||||
}
|
||||
|
||||
function ErrorState({ isCloudUser, onRetry }: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<img src={awwSnapUrl} alt="something went wrong" className={styles.img} />
|
||||
<Typography.Text className={styles.errorText}>
|
||||
Something went wrong :/ Please retry or contact support.
|
||||
</Typography.Text>
|
||||
<section className={styles.actionButtons}>
|
||||
<Button
|
||||
className={styles.retryButton}
|
||||
type="text"
|
||||
icon={<RotateCw size={16} />}
|
||||
onClick={onRetry}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.learnMore}
|
||||
onClick={(): void => handleContactSupport(isCloudUser)}
|
||||
>
|
||||
Contact Support
|
||||
</Button>
|
||||
<ArrowUpRight size={16} className={styles.learnMoreArrow} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorState;
|
||||
@@ -1,73 +0,0 @@
|
||||
.contentContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.contentHeader {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.jsonError {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
color: var(--warning-background);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.importJsonModalWrapper) {
|
||||
:global(.ant-modal-content) {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.margin) {
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
:global(.view-lines) {
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
:global(.ant-modal-footer) {
|
||||
margin-top: 0;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { red } from '@ant-design/colors';
|
||||
import MEditor, { Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Flex, Modal, Upload, UploadProps } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
CircleAlert,
|
||||
ExternalLink,
|
||||
Github,
|
||||
MonitorDot,
|
||||
MoveRight,
|
||||
Sparkles,
|
||||
} from '@signozhq/icons';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { createDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import sampleDashboard from './sampleDashboard.json';
|
||||
|
||||
import styles from './ImportJSONModal.module.scss';
|
||||
import { normalizeToPostable } from './ImportJSONModalUtils';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function ImportJSONModal({ open, onClose }: Props): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { t } = useTranslation(['dashboard', 'common']);
|
||||
const [isUploadError, setIsUploadError] = useState(false);
|
||||
const [isCreateError, setIsCreateError] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [editorValue, setEditorValue] = useState('');
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const handleUpload: UploadProps['onChange'] = (info) => {
|
||||
const lastFile = info.fileList[info.fileList.length - 1];
|
||||
if (!lastFile?.originFileObj) {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event): void => {
|
||||
try {
|
||||
const target = event.target?.result;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
const parsed = JSON.parse(target.toString());
|
||||
setEditorValue(JSON.stringify(parsed, null, 2));
|
||||
setIsUploadError(false);
|
||||
} catch {
|
||||
setIsUploadError(true);
|
||||
}
|
||||
};
|
||||
reader.readAsText(lastFile.originFileObj);
|
||||
};
|
||||
|
||||
const handleImport = async (): Promise<void> => {
|
||||
try {
|
||||
setIsCreating(true);
|
||||
logEvent('Dashboard List V2: Import and next clicked', {});
|
||||
const parsed = JSON.parse(editorValue) as Record<string, unknown>;
|
||||
const payload = normalizeToPostable(parsed);
|
||||
const response = await createDashboardV2(payload);
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id }),
|
||||
);
|
||||
logEvent('Dashboard List V2: New dashboard imported successfully', {
|
||||
dashboardId: response.data?.id,
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setIsCreateError(true);
|
||||
notifications.error({
|
||||
message: error instanceof Error ? error.message : t('error_loading_json'),
|
||||
});
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
setIsUploadError(false);
|
||||
setIsCreateError(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const setEditorTheme = (monaco: Monaco): void => {
|
||||
monaco.editor.defineTheme('my-theme', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
|
||||
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
|
||||
],
|
||||
colors: { 'editor.background': Color.BG_INK_300 },
|
||||
});
|
||||
};
|
||||
|
||||
const renderError = (msg: string): JSX.Element => (
|
||||
<div className={styles.jsonError}>
|
||||
<CircleAlert size="md" color={red[7]} />
|
||||
<Typography className={styles.errorText}>{msg}</Typography>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
wrapClassName="importJsonModalWrapper"
|
||||
open={open}
|
||||
centered
|
||||
closable
|
||||
keyboard
|
||||
maskClosable
|
||||
onCancel={handleClose}
|
||||
destroyOnClose
|
||||
width="60vw"
|
||||
footer={
|
||||
<div className={styles.footer}>
|
||||
{isCreateError && renderError(t('error_loading_json'))}
|
||||
{isUploadError && renderError(t('error_upload_json'))}
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Flex gap="small">
|
||||
<Upload
|
||||
accept=".json"
|
||||
showUploadList={false}
|
||||
multiple={false}
|
||||
onChange={handleUpload}
|
||||
beforeUpload={(): boolean => false}
|
||||
action="none"
|
||||
>
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn"
|
||||
icon={<MonitorDot size={14} />}
|
||||
onClick={(): void => {
|
||||
logEvent('Dashboard List V2: Upload JSON file clicked', {});
|
||||
}}
|
||||
>
|
||||
{t('upload_json_file')}
|
||||
</Button>
|
||||
</Upload>
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn"
|
||||
icon={<Sparkles size={14} />}
|
||||
onClick={(): void => {
|
||||
setEditorValue(JSON.stringify(sampleDashboard, null, 2));
|
||||
setIsUploadError(false);
|
||||
logEvent('Dashboard List V2: Load sample clicked', {});
|
||||
}}
|
||||
>
|
||||
Load sample
|
||||
</Button>
|
||||
<a
|
||||
href="https://signoz.io/docs/dashboards/dashboard-templates/overview/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
type="default"
|
||||
className="periscope-btn"
|
||||
icon={<Github size={14} />}
|
||||
>
|
||||
{t('view_template')}
|
||||
<ExternalLink size={14} />
|
||||
</Button>
|
||||
</a>
|
||||
</Flex>
|
||||
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
loading={isCreating}
|
||||
className="periscope-btn primary"
|
||||
type="primary"
|
||||
>
|
||||
{t('import_and_next')} <MoveRight size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.contentContainer}>
|
||||
<div className={styles.contentHeader}>
|
||||
<Typography.Text>{t('import_json')}</Typography.Text>
|
||||
</div>
|
||||
<MEditor
|
||||
language="json"
|
||||
height="40vh"
|
||||
onChange={(newValue): void => setEditorValue(newValue || '')}
|
||||
value={editorValue}
|
||||
options={{
|
||||
scrollbar: { alwaysConsumeMouseWheel: false },
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
fontFamily: 'Space Mono',
|
||||
}}
|
||||
theme={isDarkMode ? 'my-theme' : 'light'}
|
||||
onMount={(_, monaco): void => {
|
||||
document.fonts.ready.then(() => {
|
||||
monaco.editor.remeasureFonts();
|
||||
});
|
||||
}}
|
||||
beforeMount={setEditorTheme}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportJSONModal;
|
||||
@@ -1,32 +0,0 @@
|
||||
import {
|
||||
DashboardtypesDashboardSpecDTO,
|
||||
DashboardtypesPostableDashboardV2DTO,
|
||||
DashboardtypesPostableDashboardV2MetadataDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
// Accept either a complete PostableDashboardV2 (`{ metadata, spec }`) or a
|
||||
// bare spec — wrap the latter with default metadata so users can paste either
|
||||
// of the shapes that exist in the wild (e.g. testdata/perses.json is a bare
|
||||
// spec).
|
||||
export function normalizeToPostable(
|
||||
parsed: Record<string, unknown>,
|
||||
): DashboardtypesPostableDashboardV2DTO {
|
||||
const hasMetadata = 'metadata' in parsed;
|
||||
const hasSpec = 'spec' in parsed;
|
||||
if (hasMetadata && hasSpec) {
|
||||
const meta = parsed.metadata as DashboardtypesPostableDashboardV2MetadataDTO;
|
||||
const metadata: DashboardtypesPostableDashboardV2MetadataDTO = {
|
||||
...meta,
|
||||
schemaVersion: meta?.schemaVersion || 'v6',
|
||||
};
|
||||
return {
|
||||
metadata,
|
||||
spec: parsed.spec as DashboardtypesDashboardSpecDTO,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
metadata: { schemaVersion: 'v6' },
|
||||
spec: parsed as unknown as DashboardtypesDashboardSpecDTO,
|
||||
};
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
{
|
||||
"display": {
|
||||
"name": "NV dashboard with sections",
|
||||
"description": ""
|
||||
},
|
||||
"datasources": {
|
||||
"SigNozDatasource": {
|
||||
"default": true,
|
||||
"plugin": {
|
||||
"kind": "signoz/Datasource",
|
||||
"spec": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panels": {
|
||||
"b424e23b": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": ""
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/NumberPanel",
|
||||
"spec": {
|
||||
"formatting": {
|
||||
"unit": "s",
|
||||
"decimalPrecision": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "container.cpu.time",
|
||||
"reduceTo": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"timeAggregation": "rate"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"251df4d5": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": ""
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"fillSpans": false
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "recommendations",
|
||||
"decimalPrecision": "2"
|
||||
},
|
||||
"chartAppearance": {
|
||||
"lineInterpolation": "spline",
|
||||
"showPoints": false,
|
||||
"lineStyle": "solid",
|
||||
"fillMode": "none",
|
||||
"spanGaps": {"fillOnlyBelow": true}
|
||||
},
|
||||
"legend": {
|
||||
"position": "bottom"
|
||||
}
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "app_recommendations_counter",
|
||||
"reduceTo": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"timeAggregation": "rate"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"layouts": [
|
||||
{
|
||||
"kind": "Grid",
|
||||
"spec": {
|
||||
"display": {
|
||||
"title": "Bravo"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/b424e23b"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Grid",
|
||||
"spec": {
|
||||
"display": {
|
||||
"title": "Alpha"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/251df4d5"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
height: 44px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 6px 6px 0px 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.rightActions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sortContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.sortHeading {
|
||||
color: var(--l3-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
padding: 12px 18px 6px 14px;
|
||||
}
|
||||
|
||||
.sortDivider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--l1-border);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.sortButton {
|
||||
text-align: start;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
padding: 12px 18px 12px 14px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.configureContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.configureButton {
|
||||
display: flex;
|
||||
text-align: start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
color: var(--l2-foreground);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.14px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
:global(.sortDashboardsPopover) {
|
||||
:global(.ant-popover-inner) {
|
||||
display: flex;
|
||||
padding: 0px;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.configureGroupPopover) {
|
||||
:global(.ant-popover-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
padding: 0px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import { Button, Popover, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
ArrowDownWideNarrow,
|
||||
Check,
|
||||
Ellipsis,
|
||||
HdmiPort,
|
||||
} from '@signozhq/icons';
|
||||
|
||||
import type {
|
||||
SortColumn,
|
||||
SortOrder,
|
||||
} from '../../hooks/useDashboardsListQueryParams';
|
||||
|
||||
import styles from './ListHeader.module.scss';
|
||||
|
||||
interface Props {
|
||||
sortColumn: SortColumn;
|
||||
onSortChange: (column: SortColumn) => void;
|
||||
sortOrder: SortOrder;
|
||||
onOrderChange: (order: SortOrder) => void;
|
||||
onConfigureMetadata: () => void;
|
||||
}
|
||||
|
||||
function ListHeader({
|
||||
sortColumn,
|
||||
onSortChange,
|
||||
sortOrder,
|
||||
onOrderChange,
|
||||
onConfigureMetadata,
|
||||
}: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Typography.Text className={styles.label}>All Dashboards</Typography.Text>
|
||||
<section className={styles.rightActions}>
|
||||
<Tooltip title="Sort">
|
||||
<Popover
|
||||
trigger="click"
|
||||
content={
|
||||
<div className={styles.sortContent}>
|
||||
<Typography.Text className={styles.sortHeading}>
|
||||
Sort By
|
||||
</Typography.Text>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.sortButton}
|
||||
onClick={(): void => onSortChange('name')}
|
||||
data-testid="sort-by-name"
|
||||
>
|
||||
Name
|
||||
{sortColumn === 'name' && <Check size={14} />}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.sortButton}
|
||||
onClick={(): void => onSortChange('created_at')}
|
||||
data-testid="sort-by-last-created"
|
||||
>
|
||||
Last created
|
||||
{sortColumn === 'created_at' && <Check size={14} />}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.sortButton}
|
||||
onClick={(): void => onSortChange('updated_at')}
|
||||
data-testid="sort-by-last-updated"
|
||||
>
|
||||
Last updated
|
||||
{sortColumn === 'updated_at' && <Check size={14} />}
|
||||
</Button>
|
||||
<div className={styles.sortDivider} />
|
||||
<Typography.Text className={styles.sortHeading}>Order</Typography.Text>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.sortButton}
|
||||
onClick={(): void => onOrderChange('asc')}
|
||||
data-testid="sort-order-asc"
|
||||
>
|
||||
Ascending
|
||||
{sortOrder === 'asc' && <Check size={14} />}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={styles.sortButton}
|
||||
onClick={(): void => onOrderChange('desc')}
|
||||
data-testid="sort-order-desc"
|
||||
>
|
||||
Descending
|
||||
{sortOrder === 'desc' && <Check size={14} />}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
rootClassName="sortDashboardsPopover"
|
||||
placement="bottomRight"
|
||||
arrow={false}
|
||||
>
|
||||
<ArrowDownWideNarrow size={14} data-testid="sort-by" />
|
||||
</Popover>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
trigger="click"
|
||||
content={
|
||||
<div className={styles.configureContent}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<HdmiPort size={14} />}
|
||||
className={styles.configureButton}
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onConfigureMetadata();
|
||||
}}
|
||||
>
|
||||
Configure metadata
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
rootClassName="configureGroupPopover"
|
||||
placement="bottomRight"
|
||||
arrow={false}
|
||||
>
|
||||
<Ellipsis size={14} />
|
||||
</Popover>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListHeader;
|
||||
@@ -1,11 +0,0 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
height: 125px;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Skeleton } from 'antd';
|
||||
|
||||
import styles from './LoadingState.module.scss';
|
||||
|
||||
function LoadingState(): JSX.Element {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Skeleton.Input active size="large" className={styles.skeleton} />
|
||||
<Skeleton.Input active size="large" className={styles.skeleton} />
|
||||
<Skeleton.Input active size="large" className={styles.skeleton} />
|
||||
<Skeleton.Input active size="large" className={styles.skeleton} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadingState;
|
||||
@@ -1,12 +0,0 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 320px;
|
||||
gap: 8px;
|
||||
padding: 105px 190px;
|
||||
margin-top: 16px;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed var(--l1-border);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
|
||||
|
||||
import styles from './NoResultsState.module.scss';
|
||||
|
||||
interface Props {
|
||||
searchString: string;
|
||||
}
|
||||
|
||||
function NoResultsState({ searchString }: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<img src={emptyStateUrl} alt="img" height={32} width={32} />
|
||||
<Typography.Text>
|
||||
No dashboards found for {searchString}. Create a new dashboard?
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoResultsState;
|
||||
@@ -1,25 +0,0 @@
|
||||
import { ChangeEvent } from 'react';
|
||||
import { Input } from 'antd';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Search } from '@signozhq/icons';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
function SearchBar({ value, onChange }: Props): JSX.Element {
|
||||
return (
|
||||
<Input
|
||||
placeholder="Search by name, description, or tags..."
|
||||
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
|
||||
value={value}
|
||||
data-testid="dashboards-list-search"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||
onChange(e.target.value)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchBar;
|
||||
@@ -1,36 +0,0 @@
|
||||
import {
|
||||
parseAsInteger,
|
||||
parseAsString,
|
||||
parseAsStringLiteral,
|
||||
useQueryState,
|
||||
type Options,
|
||||
type UseQueryStateReturn,
|
||||
} from 'nuqs';
|
||||
|
||||
export const SORT_COLUMNS = ['updated_at', 'created_at', 'name'] as const;
|
||||
export type SortColumn = (typeof SORT_COLUMNS)[number];
|
||||
|
||||
export const SORT_ORDERS = ['asc', 'desc'] as const;
|
||||
export type SortOrder = (typeof SORT_ORDERS)[number];
|
||||
|
||||
const opts: Options = { history: 'push' };
|
||||
|
||||
export const useSortColumn = (): UseQueryStateReturn<SortColumn, SortColumn> =>
|
||||
useQueryState(
|
||||
'sort',
|
||||
parseAsStringLiteral(SORT_COLUMNS)
|
||||
.withDefault('updated_at')
|
||||
.withOptions(opts),
|
||||
);
|
||||
|
||||
export const useSortOrder = (): UseQueryStateReturn<SortOrder, SortOrder> =>
|
||||
useQueryState(
|
||||
'order',
|
||||
parseAsStringLiteral(SORT_ORDERS).withDefault('desc').withOptions(opts),
|
||||
);
|
||||
|
||||
export const usePage = (): UseQueryStateReturn<number, number> =>
|
||||
useQueryState('page', parseAsInteger.withDefault(1).withOptions(opts));
|
||||
|
||||
export const useSearch = (): UseQueryStateReturn<string, string> =>
|
||||
useQueryState('search', parseAsString.withDefault('').withOptions(opts));
|
||||
@@ -1,3 +1,9 @@
|
||||
import DashboardsListPageV2 from './DashboardsListPageV2';
|
||||
function DashboardsListPageV2(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboards List Page V2</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardsListPageV2;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { DashboardtypesGettableDashboardWithPinDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export type DashboardListItem = DashboardtypesGettableDashboardWithPinDTO;
|
||||
|
||||
export const tagsToStrings = (
|
||||
tags: { key: string; value: string }[] | null | undefined,
|
||||
): string[] =>
|
||||
(tags ?? []).map((tag) =>
|
||||
tag.key === tag.value ? tag.key : `${tag.key}:${tag.value}`,
|
||||
);
|
||||
|
||||
// Build a dashboard list filter DSL query from a free-text search input.
|
||||
// Matches against name, description, and tags; empty input returns undefined so the
|
||||
// `query` param is omitted from the API call.
|
||||
// This is only for the old UX that uses v2. The new UX will actually have DSL search input,
|
||||
// so it will pass the DSL directly and not use this function.
|
||||
export const buildSearchDSL = (search: string): string | undefined => {
|
||||
const trimmed = search.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const escaped = trimmed.replace(/'/g, "''");
|
||||
return `name CONTAINS '${escaped}' or description CONTAINS '${escaped}' or tags.key CONTAINS '${escaped}' or tags.value CONTAINS '${escaped}'`;
|
||||
};
|
||||
@@ -16,7 +16,7 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import history from 'lib/history';
|
||||
import history, { hasInAppHistory } from 'lib/history';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CalendarClock,
|
||||
@@ -96,13 +96,7 @@ function TraceDetailsHeader({
|
||||
}, [traceID]);
|
||||
|
||||
const handlePreviousBtnClick = useCallback((): void => {
|
||||
const isSpaNavigate =
|
||||
document.referrer &&
|
||||
// oxlint-disable-next-line signoz/no-raw-absolute-path
|
||||
new URL(document.referrer).origin === window.location.origin;
|
||||
const hasBackHistory = window.history.length > 1;
|
||||
|
||||
if (isSpaNavigate && hasBackHistory) {
|
||||
if (hasInAppHistory()) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
@@ -130,6 +124,7 @@ function TraceDetailsHeader({
|
||||
size="md"
|
||||
className={styles.backBtn}
|
||||
onClick={handlePreviousBtnClick}
|
||||
aria-label="Back"
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import TraceDetailsHeader from '../TraceDetailsHeader';
|
||||
|
||||
const mockGoBack = jest.fn();
|
||||
const mockPush = jest.fn();
|
||||
const mockHasInAppHistory = jest.fn();
|
||||
|
||||
jest.mock('lib/history', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
goBack: (): void => mockGoBack(),
|
||||
push: (path: string): void => mockPush(path),
|
||||
replace: jest.fn(),
|
||||
location: { pathname: '/', search: '' },
|
||||
listen: (): (() => void) => (): void => undefined,
|
||||
},
|
||||
hasInAppHistory: (): boolean => mockHasInAppHistory(),
|
||||
}));
|
||||
|
||||
const baseProps = {
|
||||
filterMetadata: {
|
||||
startTime: 0,
|
||||
endTime: 1,
|
||||
traceId: 'trace-123',
|
||||
},
|
||||
onFilteredSpansChange: jest.fn(),
|
||||
isDataLoaded: false,
|
||||
};
|
||||
|
||||
describe('TraceDetailsHeader – back button', () => {
|
||||
beforeEach(() => {
|
||||
mockGoBack.mockClear();
|
||||
mockPush.mockClear();
|
||||
mockHasInAppHistory.mockReset();
|
||||
});
|
||||
|
||||
it('calls history.goBack() when there is in-app SPA history', () => {
|
||||
mockHasInAppHistory.mockReturnValue(true);
|
||||
render(<TraceDetailsHeader {...baseProps} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /back/i }));
|
||||
|
||||
expect(mockGoBack).toHaveBeenCalledTimes(1);
|
||||
expect(mockPush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('pushes to the traces explorer route when there is no in-app SPA history', () => {
|
||||
mockHasInAppHistory.mockReturnValue(false);
|
||||
render(<TraceDetailsHeader {...baseProps} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /back/i }));
|
||||
|
||||
expect(mockPush).toHaveBeenCalledTimes(1);
|
||||
expect(mockPush).toHaveBeenCalledWith(ROUTES.TRACES_EXPLORER);
|
||||
expect(mockGoBack).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -48,9 +48,7 @@
|
||||
"node_modules",
|
||||
"src/parser/*.ts",
|
||||
"src/parser/TraceOperatorParser/*.ts",
|
||||
"orval.config.ts",
|
||||
"src/pages/DashboardsListPageV2/**/*",
|
||||
"src/pages/DashboardPageV2/**/*"
|
||||
"orval.config.ts"
|
||||
],
|
||||
"include": [
|
||||
"./src",
|
||||
|
||||
Reference in New Issue
Block a user