Compare commits

..

1 Commits

Author SHA1 Message Date
Gaurav Tewari
1b90f44bba fix: tag container styles in qb 2026-05-29 19:54:55 +05:30
120 changed files with 417 additions and 5212 deletions

View File

@@ -58,6 +58,8 @@ jobs:
run: |
mkdir -p frontend
echo 'CI=1' > frontend/.env
echo 'VITE_INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' >> frontend/.env
echo 'VITE_SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> frontend/.env
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env

View File

@@ -24,6 +24,8 @@ jobs:
- name: dotenv-frontend
working-directory: frontend
run: |
echo 'VITE_INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > .env
echo 'VITE_SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> .env
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> .env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> .env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> .env

View File

@@ -52,7 +52,6 @@ jobs:
- rootuser
- serviceaccount
- querier_json_body
- querier_skip_resource_fingerprint
- ttl
sqlstore-provider:
- postgres

View File

@@ -26,7 +26,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
cmock "github.com/SigNoz/clickhouse-go-mock"
cmock "github.com/srikanthccv/ClickHouse-go-mock"
)
func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {

View File

@@ -1,9 +1,8 @@
import { useCallback, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Dot } from '@signozhq/icons';
import { Dot, Sparkles } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import Noz from 'components/Noz/Noz';
import { Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { AIAssistantEvents } from 'container/AIAssistant/events';
@@ -22,7 +21,6 @@ import FeedbackModal from './FeedbackModal';
import ShareURLModal from './ShareURLModal';
import './HeaderRightSection.styles.scss';
import { Typography } from '@signozhq/ui/typography';
interface HeaderRightSectionProps {
enableAnnouncements: boolean;
@@ -109,22 +107,21 @@ function HeaderRightSection({
</span>
) : null}
<TooltipSimple title="Noz">
<TooltipSimple title="AI Assistant">
<Button
variant="solid"
color="secondary"
className="noz-wave"
onClick={handleOpenAIAssistant}
aria-label={
showHeaderPendingBadge
? pendingUserInputCount === 1
? 'Open Noz, 1 action needs your response'
: `Open Noz, ${pendingUserInputCount} actions need your response`
: 'Open Noz'
? 'Open AI Assistant, 1 action needs your response'
: `Open AI Assistant, ${pendingUserInputCount} actions need your response`
: 'Open AI Assistant'
}
prefix={<Noz size={20} />}
prefix={<Sparkles size={14} color="var(--primary)" />}
>
<Typography.Text>Noz</Typography.Text>
AI Assistant
</Button>
</TooltipSimple>
</div>

View File

@@ -1,86 +0,0 @@
@keyframes noz-wave-wiggle {
0%,
100% {
transform: rotate(0deg);
}
25% {
transform: rotate(20deg);
}
75% {
transform: rotate(-20deg);
}
}
.noz {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
overflow: visible;
svg {
display: block;
overflow: visible;
}
:global {
.noz-arm-left {
transform-origin: 4.18383px 13.4752px;
transform: rotate(0deg);
transition: transform 0.55s cubic-bezier(0.34, 1.7, 0.5, 1);
will-change: transform;
}
.noz-arm-wiggle {
transform-origin: 4.18383px 13.4752px;
transform: rotate(0deg);
will-change: transform;
}
.noz-head {
transform-origin: 12.02px 18.37px;
transform: rotate(0deg);
transition: transform 0.5s cubic-bezier(0.34, 1.7, 0.5, 1);
will-change: transform;
}
}
&:hover {
:global(.noz-arm-left) {
transform: rotate(145deg) scale(1, 1.7);
}
:global(.noz-arm-wiggle) {
animation: noz-wave-wiggle 0.7s ease-in-out 0.2s infinite;
}
:global(.noz-head) {
transform: rotate(9deg);
}
}
}
:global(.noz-wave):hover {
:global(.noz-arm-left) {
transform: rotate(145deg) scale(1, 1.7);
}
:global(.noz-arm-wiggle) {
animation: noz-wave-wiggle 0.7s ease-in-out 0.2s infinite;
}
:global(.noz-head) {
transform: rotate(9deg);
}
}
@media (prefers-reduced-motion: reduce) {
.noz {
:global(.noz-arm-left),
:global(.noz-arm-wiggle),
:global(.noz-head) {
transition: none;
animation: none !important;
}
}
}

View File

@@ -1,100 +0,0 @@
import cx from 'classnames';
import styles from './Noz.module.scss';
interface NozProps {
size?: number;
className?: string;
}
/**
* Noz — the SigNoz AI assistant mascot. Waves on hover.
*
* Hover behavior:
* - Hovering the icon itself triggers the wave.
* - To make a parent element (e.g. a Button) trigger the wave on its own
* hover, add the class `noz-wave` to that parent.
*/
export default function Noz({ size = 24, className }: NozProps): JSX.Element {
return (
<span
className={cx(styles.noz, className)}
style={{ width: size, height: size }}
aria-hidden="true"
>
<svg
viewBox="-2 0.5 28 28"
width={size}
height={size}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* body */}
<rect
x="4.35938"
y="8.49908"
width="15.4569"
height="11.978"
rx="1.76147"
fill="#E5484D"
/>
{/* legs */}
<rect
x="6.87012"
y="19.0679"
width="3.34679"
height="3.69908"
rx="0.880734"
fill="#E5484D"
/>
<rect
x="13.916"
y="19.0679"
width="3.34679"
height="3.69908"
rx="0.880734"
fill="#E5484D"
/>
{/* right arm (static) */}
<rect
x="18.7598"
y="13.4752"
width="2.11376"
height="3.69908"
rx="0.880734"
fill="#E5484D"
/>
{/* left arm: outer group does the "lift", inner does the wiggle */}
<g className="noz-arm-left">
<g className="noz-arm-wiggle">
<rect
x="3.12695"
y="13.4752"
width="2.11376"
height="3.69908"
rx="0.880734"
fill="#E5484D"
/>
</g>
</g>
{/* head: face + eye + hat tilt together */}
<g className="noz-head">
<circle cx="12.0217" cy="14.4881" r="3.87523" fill="#F5F5F5" />
<path
d="M12.0237 12.8024C12.0237 13.7328 11.2673 14.4892 10.337 14.4892C10.0339 14.4892 9.74926 14.4101 9.50152 14.2678C9.47517 14.5551 9.49888 14.8502 9.57795 15.1428C9.93901 16.4921 11.3279 17.2933 12.6773 16.9323C14.0267 16.5712 14.8279 15.1823 14.4668 13.8329C14.1453 12.6285 13.0041 11.8616 11.8023 11.967C11.942 12.2121 12.0237 12.4967 12.0237 12.8024Z"
fill="#0A0C10"
/>
<path
d="M8.33833 7.94578L9.83358 4.31319C10.1302 3.59261 10.6676 2.99939 11.355 2.63299L13.9181 1.26684C14.1327 1.15169 14.3804 1.34885 14.3194 1.58439L13.6703 4.06892C13.6511 4.14046 13.6424 4.21374 13.6424 4.28876C13.6424 4.39868 13.6633 4.5086 13.7052 4.61154L15.0382 7.94578H11.4248L11.6307 7.32813L12.3356 7.09259C12.449 7.05421 12.5257 6.94778 12.5257 6.82739C12.5257 6.707 12.449 6.60057 12.3356 6.56218L11.6307 6.32664L11.3951 5.62176C11.3568 5.51009 11.2503 5.43333 11.1299 5.43333C11.0096 5.43333 10.9031 5.5101 10.8647 5.6235L10.6292 6.32839L9.92431 6.56393C9.8109 6.60231 9.73413 6.70874 9.73413 6.82913C9.73413 6.94952 9.8109 7.05595 9.92431 7.09434L10.6292 7.32988L10.8351 7.94752H8.33833V7.94578ZM12.1 3.43558C12.0808 3.378 12.0285 3.33962 11.9674 3.33962C11.9064 3.33962 11.854 3.378 11.8348 3.43558L11.7179 3.78802L11.3655 3.90492C11.3079 3.92411 11.2695 3.97645 11.2695 4.03752C11.2695 4.09859 11.3079 4.15093 11.3655 4.17012L11.7179 4.28702L11.8348 4.63946C11.854 4.69704 11.9064 4.73542 11.9674 4.73542C12.0285 4.73542 12.0808 4.69704 12.1 4.63946L12.2169 4.28702L12.5694 4.17012C12.6269 4.15093 12.6653 4.09859 12.6653 4.03752C12.6653 3.97645 12.6269 3.92411 12.5694 3.90492L12.2169 3.78802L12.1 3.43558ZM7.78 7.91088H15.5965C15.9053 7.91088 16.1548 8.16038 16.1548 8.4692C16.1548 8.77803 15.9053 9.02753 15.5965 9.02753H7.78C7.47118 9.02753 7.22168 8.77803 7.22168 8.4692C7.22168 8.16038 7.47118 7.91088 7.78 7.91088Z"
fill="#4E74F8"
/>
</g>
</svg>
</span>
);
}
Noz.defaultProps = {
size: 24,
className: undefined,
};

View File

@@ -0,0 +1,12 @@
.route-tab-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px;
}
.route-tab-extra {
display: flex;
align-items: center;
}

View File

@@ -70,7 +70,7 @@ describe('RouteTab component', () => {
</Router>,
);
expect(history.location.pathname).toBe('/');
fireEvent.click(screen.getByRole('tab', { name: 'Tab2' }));
fireEvent.mouseDown(screen.getByRole('tab', { name: 'Tab2' }));
expect(history.location.pathname).toBe('/tab2');
});
@@ -87,7 +87,7 @@ describe('RouteTab component', () => {
/>
</Router>,
);
fireEvent.click(screen.getByRole('tab', { name: 'Tab2' }));
fireEvent.mouseDown(screen.getByRole('tab', { name: 'Tab2' }));
expect(onChangeHandler).toHaveBeenCalled();
});
});

View File

@@ -1,10 +1,17 @@
import './RouteTab.styles.scss';
import {
generatePath,
matchPath,
useLocation,
useParams,
} from 'react-router-dom';
import { Tabs, TabsProps } from 'antd';
import {
TabsContent,
TabsList,
TabsRoot,
TabsTrigger,
} from '@signozhq/ui/tabs';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import { RouteTabProps } from './types';
@@ -16,11 +23,13 @@ interface Params {
function RouteTab({
routes,
activeKey,
defaultActiveKey,
onChangeHandler,
history,
showRightSection,
...rest
}: RouteTabProps & TabsProps): JSX.Element {
showRightSection = true,
tabBarExtraContent,
hideTabBar = false,
}: RouteTabProps): JSX.Element {
const params = useParams<Params>();
const location = useLocation();
@@ -46,38 +55,38 @@ function RouteTab({
}
};
const items = routes.map(({ Component, name, route, key }) => ({
label: name,
key,
tabKey: route,
children: <Component />,
}));
const resolvedActiveKey = currentRoute?.key || activeKey;
const extraContent =
tabBarExtraContent ??
(showRightSection && (
<HeaderRightSection enableAnnouncements={false} enableShare enableFeedback />
));
return (
<Tabs
onChange={onChange}
destroyInactiveTabPane
activeKey={currentRoute?.key || activeKey}
defaultActiveKey={currentRoute?.key || activeKey}
animated
items={items}
tabBarExtraContent={
showRightSection && (
<HeaderRightSection
enableAnnouncements={false}
enableShare
enableFeedback
/>
)
}
{...rest}
/>
<TabsRoot
value={resolvedActiveKey}
defaultValue={defaultActiveKey ?? resolvedActiveKey}
onValueChange={onChange}
>
{!hideTabBar && (
<div className="route-tab-header">
<TabsList>
{routes.map(({ name, key }) => (
<TabsTrigger key={key} value={key}>
{name}
</TabsTrigger>
))}
</TabsList>
{extraContent && <div className="route-tab-extra">{extraContent}</div>}
</div>
)}
{routes.map(({ key, Component }) => (
<TabsContent key={key} value={key}>
<Component />
</TabsContent>
))}
</TabsRoot>
);
}
RouteTab.defaultProps = {
onChangeHandler: undefined,
showRightSection: true,
};
export default RouteTab;

View File

@@ -1,5 +1,5 @@
import { TabsProps } from 'antd';
import { History } from 'history';
import { ReactNode } from 'react';
export type TabRoutes = {
name: React.ReactNode;
@@ -10,8 +10,11 @@ export type TabRoutes = {
export interface RouteTabProps {
routes: TabRoutes[];
activeKey: TabsProps['activeKey'];
activeKey: string | undefined;
defaultActiveKey?: string;
onChangeHandler?: (key: string) => void;
history: History<unknown>;
showRightSection: boolean;
showRightSection?: boolean;
tabBarExtraContent?: ReactNode;
hideTabBar?: boolean;
}

View File

@@ -1,4 +1,4 @@
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
import type { HTMLAttributes, ReactNode } from 'react';
import cx from 'classnames';
import tableStyles from './TanStackTable.module.scss';
@@ -22,19 +22,21 @@ type WithDangerousHtml = BaseProps & {
export type TanStackTableTextProps = WithChildren | WithDangerousHtml;
const TanStackTableText = forwardRef<HTMLSpanElement, TanStackTableTextProps>(
({ children, className, dangerouslySetInnerHTML, ...rest }, ref) => (
function TanStackTableText({
children,
className,
dangerouslySetInnerHTML,
...rest
}: TanStackTableTextProps): JSX.Element {
return (
<span
ref={ref}
className={cx(tableStyles.tableCellText, className)}
dangerouslySetInnerHTML={dangerouslySetInnerHTML}
{...rest}
>
{children}
</span>
),
);
TanStackTableText.displayName = 'TanStackTableText';
);
}
export default TanStackTableText;

View File

@@ -76,19 +76,4 @@
.cmd-item-icon {
margin-right: 8px;
display: flex;
align-items: center;
justify-content: center;
svg {
height: 16px !important;
width: 16px !important;
}
&.noz-icon {
svg {
height: 20px !important;
width: 20px !important;
}
}
}

View File

@@ -1,5 +1,4 @@
import React, { useEffect } from 'react';
import cx from 'classnames';
import { useLocation } from 'react-router-dom';
import {
CommandDialog,
@@ -163,11 +162,7 @@ export function CmdKPalette({
value={it.name}
className={theme === 'light' ? 'cmdk-item-light' : 'cmdk-item'}
>
<span
className={cx('cmd-item-icon', it.id === 'ai-assistant' && 'noz-icon')}
>
{it.icon}
</span>
<span className="cmd-item-icon">{it.icon}</span>
{it.name}
{it.shortcut && it.shortcut.length > 0 && (
<CommandShortcut>{it.shortcut.join(' • ')}</CommandShortcut>

View File

@@ -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',
}

View File

@@ -15,10 +15,10 @@ import {
ListMinus,
ScrollText,
Settings,
Sparkles,
TowerControl,
Workflow,
} from '@signozhq/icons';
import Noz from 'components/Noz/Noz';
import { ROLES } from 'types/roles';
export type CmdAction = {
@@ -292,11 +292,11 @@ export function createShortcutActions(deps: ActionDeps): CmdAction[] {
if (aiAssistant) {
actions.unshift({
id: 'ai-assistant',
name: 'Open Noz',
name: 'Open AI Assistant',
shortcut: ['cmd+j'],
keywords: 'noz ai assistant chat ask sparkles copilot',
section: 'Noz',
icon: <Noz size={16} />,
keywords: 'ai assistant chat ask sparkles copilot',
section: 'AI Assistant',
icon: <Sparkles size={14} />,
roles: ['ADMIN', 'EDITOR', 'VIEWER'],
perform: aiAssistant.open,
});

View File

@@ -4,8 +4,7 @@ import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Drawer } from 'antd';
import ROUTES from 'constants/routes';
import { Maximize2, Plus, X } from '@signozhq/icons';
import Noz from 'components/Noz/Noz';
import { Maximize2, MessageSquare, Plus, X } from '@signozhq/icons';
import ConversationView from '../ConversationView';
import { useAIAssistantStore } from '../store/useAIAssistantStore';
@@ -47,9 +46,9 @@ export default function AIAssistantDrawer(): JSX.Element {
closeIcon={null}
title={
<div>
<div className="noz-wave">
<Noz size={16} />
<span>Noz</span>
<div>
<MessageSquare size={16} />
<span>AI Assistant</span>
</div>
<div>

View File

@@ -4,8 +4,7 @@ import { useHistory, useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import ROUTES from 'constants/routes';
import { History, Maximize2, Minus, Plus, X } from '@signozhq/icons';
import Noz from 'components/Noz/Noz';
import { History, Maximize2, Minus, Plus, Sparkles, X } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
@@ -143,15 +142,15 @@ export default function AIAssistantModal(): JSX.Element | null {
className={styles.backdrop}
role="dialog"
aria-modal="true"
aria-label="Noz"
aria-label="AI Assistant"
onClick={handleBackdropClick}
>
<div className={styles.modal}>
{/* Header */}
<div className={styles.header}>
<div className={`${styles.title} noz-wave`}>
<Noz size={16} />
<span>Noz</span>
<div className={styles.title}>
<Sparkles size={16} color="var(--primary)" />
<span>AI Assistant</span>
<kbd className={styles.shortcut}>
<span></span>
<span>J</span>

View File

@@ -3,8 +3,7 @@ import { matchPath, useHistory, useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import ROUTES from 'constants/routes';
import { History, Maximize2, Plus, X } from '@signozhq/icons';
import Noz from 'components/Noz/Noz';
import { History, Maximize2, Plus, Sparkles, X } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
@@ -138,9 +137,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div className={styles.resizeHandle} onMouseDown={handleResizeMouseDown} />
<div className={styles.header}>
<div className={`${styles.title} noz-wave`}>
<Noz size={18} />
<span>Noz</span>
<div className={styles.title}>
<Sparkles size={18} color="var(--primary)" />
<span>AI Assistant</span>
</div>
<div className={styles.actions}>

View File

@@ -4,7 +4,7 @@ import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import Noz from 'components/Noz/Noz';
import { Bot } from '@signozhq/icons';
import { AIAssistantEvents, AIAssistantOpenSource } from '../events';
import { normalizePage } from '../hooks/useAIAssistantAnalyticsContext';
@@ -42,15 +42,15 @@ export default function AIAssistantTrigger(): JSX.Element | null {
}
return (
<TooltipSimple title="Noz">
<TooltipSimple title="AI Assistant">
<Button
variant="solid"
color="primary"
className={`${styles.trigger} noz-wave`}
className={styles.trigger}
onClick={handleOpen}
aria-label="Open Noz"
aria-label="Open AI Assistant"
>
<Noz size={24} />
<Bot size={20} />
</Button>
</TooltipSimple>
);

View File

@@ -28,6 +28,7 @@
.emptyIcon {
margin-bottom: 4px;
opacity: 0.85;
}
.emptyTitle {

View File

@@ -7,8 +7,8 @@ import {
ChartBar,
Search,
Zap,
Sparkles,
} from '@signozhq/icons';
import Noz from 'components/Noz/Noz';
import logEvent from 'api/common/logEvent';
@@ -177,10 +177,10 @@ export default function VirtualizedMessages({
if (messages.length === 0 && !showStreamingSlot) {
return (
<div className={styles.empty}>
<div className={`${styles.emptyIcon} noz-wave`}>
<Noz size={48} />
<div className={styles.emptyIcon}>
<Sparkles size={24} color="var(--primary)" />
</div>
<h3 className={styles.emptyTitle}>Noz</h3>
<h3 className={styles.emptyTitle}>SigNoz AI Assistant</h3>
<p className={styles.emptySubtitle}>
Ask questions about your traces, logs, metrics, and infrastructure.
</p>

View File

@@ -1,70 +0,0 @@
.settings-tabs {
.ant-tabs-nav-list {
height: 32px;
flex-shrink: 0;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
transition: opacity 0.1s !important;
.ant-tabs-tab + .ant-tabs-tab {
margin: 0px;
}
.ant-tabs-tab:not(:last-child) {
border-right: 1px solid var(--l1-border) !important;
}
.overview-btn {
width: 114px;
display: flex;
align-items: center;
justify-content: center;
}
.variables-btn {
width: 114px;
display: flex;
align-items: center;
justify-content: center;
}
.public-dashboard-btn {
width: 150px;
display: flex;
align-items: center;
justify-content: center;
&.disabled-btn {
opacity: 0.5;
cursor: not-allowed;
}
}
.ant-tabs-ink-bar {
display: none;
}
.ant-tabs-tab-active {
.overview-btn {
border-radius: 2px 0px 0px 2px;
background: var(--l1-border);
}
.variables-btn {
border-radius: 2px 0px 0px 2px;
background: var(--l1-border);
}
.public-dashboard-btn {
border-radius: 2px 0px 0px 2px;
background: var(--l1-border);
}
}
}
.ant-tabs-nav::before {
border-bottom: none;
}
}

View File

@@ -1,4 +1,4 @@
import { Button, Tabs, Tooltip } from 'antd';
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { Braces, Globe, Table } from '@signozhq/icons';
import { useAppContext } from 'providers/App/App';
@@ -9,8 +9,6 @@ import DashboardVariableSettings from './DashboardVariableSettings';
import GeneralDashboardSettings from './General';
import PublicDashboardSetting from './PublicDashboard';
import './DashboardSettingsContent.styles.scss';
function DashboardSettings({
variablesSettingsTabHandle,
}: {
@@ -21,49 +19,26 @@ function DashboardSettings({
const enablePublicDashboard = isCloudUser || isEnterpriseSelfHostedUser;
const publicDashboardItem = {
label: (
<Tooltip
title={
user?.role !== USER_ROLES.ADMIN
? 'Only admins can publish / manage public dashboards'
: ''
}
placement="right"
>
<Button
type="text"
icon={<Globe size={14} />}
className={`public-dashboard-btn ${
user?.role !== USER_ROLES.ADMIN ? 'disabled-btn' : ''
}`}
>
Publish
</Button>
</Tooltip>
),
const publicDashboardItem: TabItemProps = {
key: 'public-dashboard',
label: 'Publish',
prefixIcon: <Globe size={14} />,
children: <PublicDashboardSetting />,
disabled: user?.role !== USER_ROLES.ADMIN,
disabledReason: 'Only admins can publish / manage public dashboards',
};
const items = [
const items: TabItemProps[] = [
{
label: (
<Button type="text" icon={<Table size={14} />} className="overview-btn">
Overview
</Button>
),
key: 'general',
label: 'Overview',
prefixIcon: <Table size={14} />,
children: <GeneralDashboardSettings />,
},
{
label: (
<Button type="text" icon={<Braces size={14} />} className="variables-btn">
Variables
</Button>
),
key: 'variables',
label: 'Variables',
prefixIcon: <Braces size={14} />,
children: (
<DashboardVariableSettings
variablesSettingsTabHandle={variablesSettingsTabHandle}
@@ -73,7 +48,7 @@ function DashboardSettings({
...(enablePublicDashboard ? [publicDashboardItem] : []),
];
return <Tabs items={items} animated className="settings-tabs" />;
return <Tabs items={items} />;
}
export default DashboardSettings;

View File

@@ -1,5 +1,4 @@
import { Button, Tabs, TabsProps } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
import { CableCar, Group } from '@signozhq/icons';
import { IntegrationDetailedProps } from 'types/api/integrations/types';
@@ -22,18 +21,11 @@ function IntegrationDetailContent(
): JSX.Element {
const { activeDetailTab, integrationData, integrationId, setActiveDetailTab } =
props;
const items: TabsProps['items'] = [
const items: TabItemProps[] = [
{
key: 'overview',
label: (
<Button
type="text"
className="integration-tab-btns"
icon={<CableCar size={14} />}
>
<Typography.Text className="typography">Overview</Typography.Text>
</Button>
),
label: 'Overview',
prefixIcon: <CableCar size={14} />,
children: (
<Overview
categories={integrationData.categories}
@@ -44,15 +36,8 @@ function IntegrationDetailContent(
},
{
key: 'configuration',
label: (
<Button
type="text"
className="integration-tab-btns"
icon={<ConfigureIcon />}
>
<Typography.Text className="typography">Configure</Typography.Text>
</Button>
),
label: 'Configure',
prefixIcon: <ConfigureIcon />,
children: (
<Configure
configuration={integrationData.configuration}
@@ -62,15 +47,8 @@ function IntegrationDetailContent(
},
{
key: 'dataCollected',
label: (
<Button
type="text"
className="integration-tab-btns"
icon={<Group size={14} />}
>
<Typography.Text className="typography">Data Collected</Typography.Text>
</Button>
),
label: 'Data Collected',
prefixIcon: <Group size={14} />,
children: (
<DataCollected
logsData={integrationData.data_collected.logs}
@@ -81,11 +59,7 @@ function IntegrationDetailContent(
];
return (
<div className="integration-detail-container">
<Tabs
activeKey={activeDetailTab}
items={items}
onChange={setActiveDetailTab}
/>
<Tabs value={activeDetailTab} items={items} onChange={setActiveDetailTab} />
</div>
);
}

View File

@@ -168,45 +168,6 @@
padding: 10px 16px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
.integration-tab-btns {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 8px 18px 8px !important;
.typography {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.integration-tab-btns:hover {
&.ant-btn-text {
background-color: unset !important;
}
}
.ant-tabs-nav-list {
gap: 24px;
}
.ant-tabs-nav {
padding: 0px !important;
}
.ant-tabs-tab {
padding: 0 !important;
}
.ant-tabs-tab + .ant-tabs-tab {
margin: 0px !important;
}
}
.uninstall-integration-bar {

View File

@@ -9,6 +9,19 @@
width: 0.2rem;
height: 0.2rem;
}
.option-value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.option-meta-data-container {
display: flex;
gap: 8px;
flex-shrink: 0;
}
}
.option-renderer-tooltip {

View File

@@ -24,7 +24,7 @@ export const StyledCheckOutlined = styled(Check)`
export const TagContainer = styled(Badge)`
&&& {
display: inline-block;
display: flex;
border-radius: 3px;
padding: 0.1rem 0.2rem;
font-weight: 300;

View File

@@ -27,7 +27,7 @@ export default function NavItem({
showIcon?: boolean;
dataTestId?: string;
}): JSX.Element {
const { label, icon, isBeta, isNew, isEarlyAccess } = item;
const { label, icon, isBeta, isNew } = item;
const handleTogglePinClick = (
event: React.MouseEvent<SVGSVGElement, MouseEvent>,
@@ -53,11 +53,7 @@ export default function NavItem({
>
{showIcon && <div className="nav-item-active-marker" />}
<div className={cx('nav-item-data', isBeta ? 'beta-tag' : '')}>
{showIcon && (
<div className={cx('nav-item-icon', isEarlyAccess ? 'noz-wave' : '')}>
{icon}
</div>
)}
{showIcon && <div className="nav-item-icon">{icon}</div>}
<div className="nav-item-label">{label}</div>
@@ -77,12 +73,6 @@ export default function NavItem({
</div>
)}
{isEarlyAccess && (
<div className="nav-item-early-access">
<Badge color="robin">Early Access</Badge>
</div>
)}
{onTogglePin && !isPinned && (
<Tooltip title="Add to shortcuts" placement="right">
<Pin

View File

@@ -579,8 +579,7 @@
}
.nav-item-beta,
.nav-item-new,
.nav-item-early-access {
.nav-item-new {
display: none;
}
@@ -624,23 +623,6 @@
background: var(--l3-background);
}
.sidenav-early-access-tag {
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings:
'case' on,
'cpsp' on,
'dlig' on,
'salt' on;
font-family: Inter;
font-size: 9px;
font-style: normal;
font-weight: 300;
line-height: 12px;
letter-spacing: 0.3px;
text-transform: uppercase;
border-radius: 12px;
}
&:not(.pinned) {
.nav-item {
.nav-item-data {
@@ -857,8 +839,7 @@
}
.nav-item-beta,
.nav-item-new,
.nav-item-early-access {
.nav-item-new {
display: block;
}
}
@@ -1031,8 +1012,7 @@
}
.nav-item-beta,
.nav-item-new,
.nav-item-early-access {
.nav-item-new {
display: block;
}
}

View File

@@ -44,7 +44,6 @@ import {
SidebarItem,
} from './sideNav.types';
import { Style } from '@signozhq/design-tokens';
import Noz from 'components/Noz/Noz';
export const getStartedMenuItem = {
key: ROUTES.GET_STARTED,
@@ -93,10 +92,9 @@ const AI_ASSISTANT_NAV_KEY = '/ai-assistant/new';
export const aiAssistantMenuItem = {
key: AI_ASSISTANT_NAV_KEY,
label: 'Noz',
icon: <Noz size={16} />,
label: 'AI Assistant',
icon: <Sparkles size={16} className="ai-assistant-icon" />,
itemKey: 'ai-assistant',
isEarlyAccess: true,
};
export const shortcutMenuItem = {

View File

@@ -14,7 +14,6 @@ export interface SidebarItem {
label?: ReactNode;
isBeta?: boolean;
isNew?: boolean;
isEarlyAccess?: boolean;
isPinned?: boolean;
children?: SidebarItem[];
isExternal?: boolean;

View File

@@ -1,10 +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.USE_DASHBOARD_V2)
?.active,
);
}

View File

@@ -9,7 +9,7 @@ import { AIAssistantEvents } from 'container/AIAssistant/events';
import { normalizePage } from 'container/AIAssistant/hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
import { VariantContext } from 'container/AIAssistant/VariantContext';
import Noz from 'components/Noz/Noz';
import { Sparkles } from '@signozhq/icons';
import styles from './AIAssistantPage.module.scss';
import ConversationsList from 'container/AIAssistant/components/ConversationsList';
@@ -116,9 +116,9 @@ export default function AIAssistantPage(): JSX.Element {
<VariantContext.Provider value="page">
<div className={styles.page}>
<div className={styles.header}>
<div className={`${styles.title} noz-wave`}>
<Noz size={18} />
<span>Noz</span>
<div className={styles.title}>
<Sparkles size={16} color="var(--primary)" />
<span>AI Assistant</span>
</div>
</div>

View File

@@ -1,5 +0,0 @@
function DashboardPageV2(): JSX.Element {
return <>DashboardPageV2</>;
}
export default DashboardPageV2;

View File

@@ -1,3 +1,8 @@
import DashboardPageV2 from './DashboardPageV2';
function DashboardPageV2(): JSX.Element {
return (
<div>
<h1>Dashboard Page V2</h1>
</div>
);
}
export default DashboardPageV2;

View File

@@ -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: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px;
letter-spacing: -0.07px;
}

View File

@@ -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&apos;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;

View File

@@ -1,28 +0,0 @@
.content {
display: flex;
flex-direction: column;
}
// Make signoz ghost-Button rows fill the popover and left-align their label.
.menuItem {
width: 100%;
justify-content: flex-start;
}
:global(.dashboardActionsPopover) {
:global(.ant-popover-inner) {
width: 200px;
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;
}
}

View File

@@ -1,103 +0,0 @@
import { Popover } from 'antd';
import { Button } from '@signozhq/ui/button';
import {
Expand,
EllipsisVertical,
Link2,
SquareArrowOutUpRight,
} from '@signozhq/icons';
import { useCopyToClipboard } from 'react-use';
import { getAbsoluteUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
import DeleteActionItem from './DeleteActionItem';
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
color="secondary"
className={styles.menuItem}
prefix={<Expand size={14} />}
onClick={onView}
testId="dashboard-action-view"
>
View
</Button>
<Button
color="secondary"
className={styles.menuItem}
prefix={<SquareArrowOutUpRight size={14} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
openInNewTab(link);
}}
testId="dashboard-action-open-new-tab"
>
Open in New Tab
</Button>
<Button
color="secondary"
className={styles.menuItem}
prefix={<Link2 size={14} />}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(getAbsoluteUrl(link));
}}
testId="dashboard-action-copy-link"
>
Copy Link
</Button>
<DeleteActionItem
dashboardId={dashboardId}
dashboardName={dashboardName}
createdBy={createdBy}
isLocked={isLocked}
/>
</div>
}
placement="bottomRight"
arrow={false}
rootClassName="dashboardActionsPopover"
trigger="click"
>
<Button
size="icon"
variant="ghost"
color="secondary"
testId="dashboard-action-icon"
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
}}
>
<EllipsisVertical size={14} />
</Button>
</Popover>
);
}
export default ActionsPopover;

View File

@@ -1,122 +0,0 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQueryClient } from 'react-query';
import { Modal, Tooltip } from 'antd';
import { Button } from '@signozhq/ui/button';
import { CircleAlert, Trash2 } from '@signozhq/icons';
import { toast } from '@signozhq/ui/sonner';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import deleteDashboard from 'api/v1/dashboards/id/delete';
import { invalidateListDashboardsV2 } from 'api/generated/services/dashboard';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { USER_ROLES } from 'types/roles';
import styles from './ActionsPopover.module.scss';
interface Props {
dashboardId: string;
dashboardName: string;
createdBy: string;
isLocked: boolean;
}
function DeleteActionItem({
dashboardId,
dashboardName,
createdBy,
isLocked,
}: Props): JSX.Element {
const { t } = useTranslation(['dashboard']);
const { user } = useAppContext();
const { showErrorModal } = useErrorModal();
const queryClient = useQueryClient();
const [modal, contextHolder] = Modal.useModal();
const isAuthor = user?.email === createdBy;
const isDisabled = isLocked || (user.role === USER_ROLES.VIEWER && !isAuthor);
const { mutate: runDelete } = useMutation({
mutationFn: () => deleteDashboard({ id: dashboardId }),
onSuccess: async () => {
toast.success(
t('dashboard:delete_dashboard_success', { name: dashboardName }),
);
await invalidateListDashboardsV2(queryClient);
},
onError: (error: APIError) => {
showErrorModal(error);
},
});
const openConfirm = useCallback((): void => {
const { destroy } = modal.confirm({
title: (
<Typography.Title level={5}>
Are you sure you want to delete the
<span style={{ color: 'var(--danger-background)', fontWeight: 500 }}>
{' '}
{dashboardName}{' '}
</span>
dashboard?
</Typography.Title>
),
icon: (
<CircleAlert
style={{ color: 'var(--danger-background)', marginInlineEnd: '12px' }}
size="3xl"
/>
),
okText: 'Delete',
okButtonProps: {
danger: true,
onClick: (e): void => {
e.preventDefault();
e.stopPropagation();
runDelete(undefined, { onSettled: () => destroy() });
},
},
centered: true,
});
}, [modal, dashboardName, runDelete]);
const tooltip = ((): string => {
if (!isLocked) {
return '';
}
if (user.role === USER_ROLES.ADMIN || isAuthor) {
return t('dashboard:locked_dashboard_delete_tooltip_admin_author');
}
return t('dashboard:locked_dashboard_delete_tooltip_editor');
})();
return (
<>
<Divider />
<Tooltip placement="left" title={tooltip}>
<Button
variant="ghost"
color="destructive"
className={styles.menuItem}
prefix={<Trash2 size={14} />}
disabled={isDisabled}
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
if (!isDisabled) {
openConfirm();
}
}}
testId="dashboard-action-delete"
>
Delete Dashboard
</Button>
</Tooltip>
{contextHolder}
</>
);
}
export default DeleteActionItem;

View File

@@ -1,164 +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: 10px;
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: var(--font-weight-medium);
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: inline-flex;
gap: 8px;
align-items: center;
color: var(--l2-foreground);
}
.formattedTimeText {
font-family: Inter;
font-size: 12.805px;
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 16.463px;
letter-spacing: -0.064px;
color: var(--l2-foreground);
}
.user {
display: flex;
align-items: center;
gap: 8px;
}
.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: var(--font-weight-normal);
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: 10px;
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;
}
}

View File

@@ -1,218 +0,0 @@
import { useEffect, useState } from 'react';
import { Button, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Switch } from '@signozhq/ui/switch';
import { CalendarClock, Check, Clock4 } from '@signozhq/icons';
import { get } from 'lodash-es';
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { useTimezone } from 'providers/Timezone';
import { lastUpdatedLabel, 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 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?.image || Base64Icons[0];
const previewName = previewDashboard?.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 && (
<span className={styles.formattedTime}>
<CalendarClock size={14} />
<Typography.Text className={styles.formattedTimeText}>
{formattedCreatedAt}
</Typography.Text>
</span>
)}
{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 && (
<span className={styles.formattedTime}>
<CalendarClock size={14} />
<Typography.Text className={styles.formattedTimeText}>
{lastUpdatedLabel(previewUpdatedAt)}
</Typography.Text>
</span>
)}
{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
value
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
value
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
value={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
value={draftColumns.updatedBy}
onChange={(check): void =>
setDraftColumns((prev) => ({
...prev,
[DynamicColumns.UPDATED_BY]: check,
}))
}
/>
</div>
</div>
</div>
</Modal>
);
}
export default ConfigureMetadataModal;

View File

@@ -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 ?? {}),
},
}),
},
),
);

View File

@@ -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: 6px 12px;
}
.textButton {
display: flex;
width: 153px;
align-items: center;
height: 32px;
padding: 6px 12px;
justify-content: center;
gap: 6px;
border-radius: 2px;
background: var(--primary-background);
color: var(--l1-foreground);
}
:global(.createDashboardMenuOverlay) {
width: 200px;
}

View File

@@ -1,119 +0,0 @@
import { useMemo } from 'react';
// eslint-disable-next-line signoz/no-antd-components -- TODO: migrate Dropdown to @signozhq/ui/dropdown-menu
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;

View File

@@ -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: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
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: var(--font-size-xs);
font-weight: var(--font-weight-normal);
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: var(--font-size-xs);
font-weight: var(--font-weight-normal);
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;
}
}

View File

@@ -1,154 +0,0 @@
import { Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Badge } from '@signozhq/ui/badge';
import { CalendarClock } from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import { generatePath } from 'react-router-dom';
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 { lastUpdatedLabel, 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 DashboardRow({
dashboard,
index,
canAct,
showUpdatedAt,
showUpdatedBy,
}: Props): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const id = dashboard.id;
const name = dashboard.spec?.display?.name ?? '';
const image = dashboard.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.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) => (
<Badge className={styles.tag} key={tag}>
{tag}
</Badge>
))}
{tags.length > 3 && (
<Badge className={styles.tag} key={tags[3]}>
+ <span> {tags.length - 3} </span>
</Badge>
)}
</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;

View File

@@ -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;
}

View File

@@ -1,277 +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 { toast } from '@signozhq/ui/sonner';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { toAPIError } from 'utils/errorUtils';
import {
usePage,
useSearch,
useSortColumn,
useSortOrder,
type SortColumn,
type SortOrder,
} from '../../hooks/useDashboardsListQueryParams';
import type { DashboardListItem } from '../../utils';
import ConfigureMetadataModal from '../ConfigureMetadataModal/ConfigureMetadataModal';
import { useDashboardsListVisibleColumnsStore } from '../ConfigureMetadataModal/useDynamicColumns';
import CreateDashboardDropdown from '../CreateDashboardDropdown/CreateDashboardDropdown';
import ImportJSONModal from '../ImportJSONModal/ImportJSONModal';
import ListHeader from '../ListHeader/ListHeader';
import EmptyState from '../states/EmptyState/EmptyState';
import ErrorState from '../states/ErrorState/ErrorState';
import LoadingState from '../states/LoadingState/LoadingState';
import NoResultsState from '../states/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 { 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);
// Keep the local input in sync with external searchString changes
// (browser back/forward, deep link). User typing only mutates
// searchInput, so this won't fight with in-flight edits.
useEffect(() => {
setSearchInput(searchString);
}, [searchString]);
const handleSubmitSearch = useCallback((): void => {
const next = searchInput.trim();
if (next === searchString) {
return;
}
void setSearchString(next);
void setPage(1);
}, [searchInput, searchString, setSearchString, setPage]);
const listParams = useMemo(
() => ({
query: searchString.trim() || undefined,
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 apiError = useMemo(
() => (error ? toAPIError(error) : undefined),
[error],
);
const errorHttpStatus = apiError?.getHttpStatusCode();
const errorMessage = apiError?.getErrorMessage();
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({
schemaVersion: 'v6',
// Backend requires `name` (immutable, server-side identifier);
// asking it to generate one keeps the UI's "new dashboard" flow.
generateName: true,
tags: null,
spec: {
display: { name: t('new_dashboard_title', { ns: 'dashboard' }) },
},
});
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: created.data.id }),
);
} catch (e) {
showErrorModal(e as APIError);
toast.error((e as AxiosError).toString() || 'Failed to create dashboard');
} finally {
setCreating(false);
}
}, [safeNavigate, showErrorModal, t]);
const handleImportToggle = useCallback((): void => {
logEvent('Dashboard List V2: Import JSON clicked', {});
setIsImportOpen((s) => !s);
}, []);
const onSortChange = useCallback(
(column: SortColumn): void => {
void setSortColumn(column);
void setPage(1);
},
[setSortColumn, setPage],
);
const onOrderChange = useCallback(
(order: SortOrder): void => {
void setSortOrder(order);
void 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 && 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}
onSubmit={handleSubmitSearch}
/>
{canCreateNewDashboard && (
<CreateDashboardDropdown
canCreate={!!canCreateNewDashboard}
onCreate={handleCreateNew}
onImportJSON={handleImportToggle}
/>
)}
</div>
{error ? (
<ErrorState
isCloudUser={!!isCloudUser}
onRetry={(): void => {
refetch();
}}
httpStatus={errorHttpStatus}
errorMessage={errorMessage}
/>
) : 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;

View File

@@ -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;

View File

@@ -1,3 +0,0 @@
import DashboardsList from './DashboardsList';
export default DashboardsList;

View File

@@ -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);
}
}

View File

@@ -1,223 +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 { toast } from '@signozhq/ui/sonner';
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 { 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 { 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);
toast.error(
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')}&nbsp;
<ExternalLink size={14} />
</Button>
</a>
</Flex>
<Button
onClick={handleImport}
loading={isCreating}
className="periscope-btn primary"
type="primary"
>
{t('import_and_next')} &nbsp; <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;

View File

@@ -1,50 +0,0 @@
import {
DashboardtypesDashboardSpecDTO,
DashboardtypesPostableDashboardV2DTO,
TagtypesPostableTagDTO,
} from 'api/generated/services/sigNoz.schemas';
// Accept either a complete PostableDashboardV2 (flat shape with `spec` and
// top-level `name` / `image` / `tags` / `schemaVersion`) or a bare spec — wrap
// the latter with defaults so users can paste either shape that exists in the
// wild (e.g. testdata/perses.json is a bare spec). The legacy nested
// `{ metadata: { ... }, spec }` shape is also accepted and flattened.
//
// The backend requires `name` (immutable identifier); if the payload doesn't
// carry one, fall back to `generateName: true` so the server assigns one.
export function normalizeToPostable(
parsed: Record<string, unknown>,
): DashboardtypesPostableDashboardV2DTO {
const hasSpec = 'spec' in parsed;
const legacyMeta = parsed.metadata as
| {
schemaVersion?: string;
name?: string;
image?: string;
tags?: TagtypesPostableTagDTO[] | null;
}
| undefined;
const resolvedName = (parsed.name as string | undefined) ?? legacyMeta?.name;
if (hasSpec) {
return {
schemaVersion:
(parsed.schemaVersion as string) || legacyMeta?.schemaVersion || 'v6',
...(resolvedName ? { name: resolvedName } : { generateName: true }),
image: (parsed.image as string) ?? legacyMeta?.image,
tags:
(parsed.tags as TagtypesPostableTagDTO[] | null) ??
legacyMeta?.tags ??
null,
spec: parsed.spec as DashboardtypesDashboardSpecDTO,
};
}
return {
schemaVersion: 'v6',
generateName: true,
tags: null,
spec: parsed as unknown as DashboardtypesDashboardSpecDTO,
};
}

View File

@@ -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"
}
}
]
}
}
]
}

View File

@@ -1,182 +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: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px;
letter-spacing: -0.07px;
}
.rightActions {
display: flex;
gap: 4px;
color: white;
}
// Shared trigger button for the sort + configure-group icons in the right
// actions cluster. Provides a square hover/active background so users know
// which icon they're targeting.
.iconTrigger {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
background: transparent;
border: none;
border-radius: 4px;
color: inherit;
cursor: pointer;
transition: background 120ms ease;
&:hover,
&:focus-visible {
background: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
outline: none;
}
&:active,
&[aria-expanded='true'] {
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
}
}
.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: var(--font-weight-semibold);
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: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: normal;
letter-spacing: 0.14px;
padding: 12px 18px 12px 14px;
height: auto;
}
.configureContent {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 4px;
}
.configureItem {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 8px 12px;
background: transparent;
border: none;
border-radius: 4px;
color: var(--l2-foreground);
font-family: Inter;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
line-height: normal;
letter-spacing: 0.14px;
text-align: left;
cursor: pointer;
transition: background 120ms ease;
&:hover,
&:focus-visible {
background: color-mix(in srgb, var(--l1-foreground) 10%, transparent);
outline: none;
}
&:active {
background: color-mix(in srgb, var(--l1-foreground) 18%, transparent);
}
}
.configureIcon {
display: inline-flex;
width: 16px;
height: 16px;
flex: 0 0 16px;
align-items: center;
justify-content: center;
}
: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;
}
}

View File

@@ -1,145 +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}
>
<button
type="button"
className={styles.iconTrigger}
data-testid="sort-by"
aria-label="Sort"
>
<ArrowDownWideNarrow size={14} />
</button>
</Popover>
</Tooltip>
<Popover
trigger="click"
content={
<div className={styles.configureContent}>
<button
type="button"
className={styles.configureItem}
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
onConfigureMetadata();
}}
data-testid="configure-metadata-trigger"
>
<span className={styles.configureIcon}>
<HdmiPort size={14} />
</span>
<span>Configure metadata</span>
</button>
</div>
}
rootClassName="configureGroupPopover"
placement="bottomRight"
arrow={false}
>
<button
type="button"
className={styles.iconTrigger}
aria-label="More options"
>
<Ellipsis size={14} />
</button>
</Popover>
</section>
</div>
);
}
export default ListHeader;

View File

@@ -1,24 +0,0 @@
.submit {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
padding: 0;
background: transparent;
border: none;
border-radius: 3px;
color: inherit;
cursor: pointer;
transition: background 120ms ease;
&:hover,
&:focus-visible {
background: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
outline: none;
}
&:active {
background: color-mix(in srgb, var(--l1-foreground) 20%, transparent);
}
}

View File

@@ -1,49 +0,0 @@
import { ChangeEvent, KeyboardEvent, MouseEvent } from 'react';
import { Input } from '@signozhq/ui/input';
import { Color } from '@signozhq/design-tokens';
import { CornerDownLeft, Search } from '@signozhq/icons';
import styles from './SearchBar.module.scss';
interface Props {
value: string;
onChange: (value: string) => void;
onSubmit: () => void;
}
function SearchBar({ value, onChange, onSubmit }: Props): JSX.Element {
return (
<Input
placeholder="Search with DSL (e.g. name CONTAINS 'foo')"
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
suffix={
<button
type="button"
className={styles.submit}
aria-label="Run search"
data-testid="dashboards-list-search-submit"
onMouseDown={(e: MouseEvent<HTMLButtonElement>): void => {
// Prevent the input's blur from firing first and double-submitting.
e.preventDefault();
}}
onClick={onSubmit}
>
<CornerDownLeft size={12} color={Color.BG_VANILLA_400} />
</button>
}
value={value}
testId="dashboards-list-search"
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
onChange(e.target.value)
}
onBlur={onSubmit}
onKeyDown={(e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
onSubmit();
}
}}
/>
);
}
export default SearchBar;

View File

@@ -1,40 +0,0 @@
.wrapper {
composes: cardWrapper from '../states.module.scss';
padding: 105px 141px;
}
.image {
width: 32px;
height: 32px;
}
.copy {
margin-top: 4px;
}
.noDashboard {
composes: bodyText from '../states.module.scss';
color: var(--l1-foreground);
font-weight: var(--font-weight-medium);
}
.info {
composes: bodyText from '../states.module.scss';
color: var(--l2-foreground);
font-weight: var(--font-weight-normal);
}
.actions {
display: flex;
gap: 24px;
align-items: center;
margin-top: 24px;
}
.learnMore {
composes: learnMoreLink from '../states.module.scss';
}
.learnMoreArrow {
composes: learnMoreArrow from '../states.module.scss';
}

View File

@@ -1,54 +0,0 @@
import { ReactNode } from 'react';
import { Button } from '@signozhq/ui/button';
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
variant="link"
color="primary"
className={styles.learnMore}
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;

View File

@@ -1,36 +0,0 @@
.wrapper {
composes: cardWrapper from '../states.module.scss';
padding: 105px 141px;
gap: 4px;
}
.img {
max-width: 100%;
}
.errorText {
composes: bodyText from '../states.module.scss';
color: var(--l1-foreground);
font-weight: var(--font-weight-medium);
}
.errorDetail {
composes: bodyText from '../states.module.scss';
color: var(--l2-foreground);
font-weight: var(--font-weight-normal);
}
.actionButtons {
display: flex;
gap: 24px;
align-items: center;
margin-top: 20px;
}
.learnMore {
composes: learnMoreLink from '../states.module.scss';
}
.learnMoreArrow {
composes: learnMoreArrow from '../states.module.scss';
}

View File

@@ -1,81 +0,0 @@
import { Button } from '@signozhq/ui/button';
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 { formatQueryErrorMessage } from '../../../utils';
import styles from './ErrorState.module.scss';
interface Props {
isCloudUser: boolean;
onRetry: () => void;
httpStatus?: number;
errorMessage?: string;
}
const GENERIC_MESSAGE =
'Something went wrong :/ Please retry or contact support.';
const INVALID_QUERY_FALLBACK = 'Please review the syntax and try again.';
function ErrorState({
isCloudUser,
onRetry,
httpStatus,
errorMessage,
}: Props): JSX.Element {
// 4xx responses are client errors — the same request will keep failing.
// Surface the BE-provided detail (e.g. DSL parse errors) and skip Retry.
const isClientError =
httpStatus !== undefined && httpStatus >= 400 && httpStatus < 500;
const cleanedDetail = formatQueryErrorMessage(errorMessage);
return (
<div className={styles.wrapper}>
<img src={awwSnapUrl} alt="something went wrong" className={styles.img} />
{isClientError ? (
<>
<Typography.Text className={styles.errorText}>
Invalid query
</Typography.Text>
<Typography.Text className={styles.errorDetail}>
{cleanedDetail || INVALID_QUERY_FALLBACK}
</Typography.Text>
</>
) : (
<Typography.Text className={styles.errorText}>
{GENERIC_MESSAGE}
</Typography.Text>
)}
<section className={styles.actionButtons}>
{!isClientError && (
<Button
variant="outlined"
color="secondary"
prefix={<RotateCw size={16} />}
onClick={onRetry}
testId="dashboards-list-retry"
>
Retry
</Button>
)}
<Button
variant="link"
color="primary"
className={styles.learnMore}
onClick={(): void => handleContactSupport(isCloudUser)}
testId="dashboards-list-contact-support"
>
Contact Support
</Button>
<ArrowUpRight size={16} className={styles.learnMoreArrow} />
</section>
</div>
);
}
export default ErrorState;

View File

@@ -1,11 +0,0 @@
.wrapper {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 16px;
}
.skeleton {
height: 125px;
width: 100%;
}

View File

@@ -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;

View File

@@ -1,5 +0,0 @@
.wrapper {
composes: cardWrapper from '../states.module.scss';
padding: 105px 190px;
gap: 8px;
}

View File

@@ -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;

View File

@@ -1,34 +0,0 @@
// Shared building blocks for the dashboards-list view states.
// Composed via CSS-modules `composes:` from each state's own SCSS.
.cardWrapper {
display: flex;
flex-direction: column;
height: 320px;
margin-top: 16px;
justify-content: center;
align-items: flex-start;
border-radius: 6px;
border: 1px dashed var(--l1-border);
}
.bodyText {
font-family: Inter;
font-size: var(--font-size-sm);
font-style: normal;
line-height: 18px;
letter-spacing: -0.07px;
}
.learnMoreLink {
composes: bodyText;
color: var(--bg-robin-400);
font-weight: var(--font-weight-medium);
padding: 0px;
}
.learnMoreArrow {
margin-left: -20px;
color: var(--bg-robin-400);
cursor: pointer;
}

View File

@@ -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));

View File

@@ -1,3 +1,9 @@
import DashboardsListPageV2 from './DashboardsListPageV2';
function DashboardsListPageV2(): JSX.Element {
return (
<div>
<h1>Dashboards List Page V2</h1>
</div>
);
}
export default DashboardsListPageV2;

View File

@@ -1,52 +0,0 @@
import dayjs from 'dayjs';
import { isEmpty } from 'lodash-es';
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}`,
);
export const 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`;
};
// Normalize BE query-parse error messages for display:
// - Drop the "invalid filter query:" prefix (the UI already says "Invalid query").
// - Backticks → double quotes for the format hint that follows the em-dash.
// - Trim surrounding whitespace.
export const formatQueryErrorMessage = (raw: string | undefined): string => {
if (!raw) {
return '';
}
return raw
.replace(/^invalid filter query:\s*/i, '')
.replace(/`([^`]+)`/g, '"$1"')
.trim();
};

View File

@@ -1,26 +1,3 @@
.service-route-tab {
margin-bottom: 64px;
.ant-tabs-nav {
&::before {
border-bottom: 1px solid var(--l1-border);
}
.ant-tabs-nav-wrap {
.ant-tabs-nav-list {
.ant-tabs-ink-bar {
background-color: var(--primary-background) !important;
}
.ant-tabs-tab {
font-size: 13px;
font-family: 'Inter';
color: var(--l1-foreground);
line-height: 20px;
letter-spacing: -0.07px;
gap: 10px;
}
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: var(--accent-primary);
}
}
}
}
}

View File

@@ -1,5 +1,5 @@
import { useParams } from 'react-router-dom';
import { Tabs, TabsProps } from 'antd';
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
import { QueryParams } from 'constants/query';
import DBCall from 'container/MetricsApplication/Tabs/DBCall';
import External from 'container/MetricsApplication/Tabs/External';
@@ -24,7 +24,7 @@ function MetricsApplication(): JSX.Element {
const urlQuery = useUrlQuery();
const { safeNavigate } = useSafeNavigate();
const items: TabsProps['items'] = [
const items: TabItemProps[] = [
{
label: TAB_KEY_VS_LABEL[MetricsApplicationTab.OVER_METRICS],
key: MetricsApplicationTab.OVER_METRICS,
@@ -53,9 +53,8 @@ function MetricsApplication(): JSX.Element {
<ApDexApplication />
<Tabs
items={items}
activeKey={activeKey}
value={activeKey}
className="service-route-tab"
destroyInactiveTabPane
onChange={onTabChange}
/>
</div>

View File

@@ -1,9 +0,0 @@
.pipeline-tabs {
.ant-tabs-content {
padding: 0 16px;
}
.ant-tabs-tabpane-hidden {
display: none !important;
}
}

View File

@@ -2,8 +2,7 @@ import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import * as Sentry from '@sentry/react';
import type { TabsProps } from 'antd';
import { Tabs } from 'antd';
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
import getPipeline from 'api/pipeline/get';
import Spinner from 'components/Spinner';
import ChangeHistory from 'container/PipelinePage/Layouts/ChangeHistory';
@@ -13,8 +12,6 @@ import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFall
import { SuccessResponse } from 'types/api';
import { Pipeline } from 'types/api/pipeline/def';
import './Pipelines.styles.scss';
const pipelineRefetchInterval = (
pipelineResponse: SuccessResponse<Pipeline> | undefined,
): number | false => {
@@ -46,7 +43,7 @@ function Pipelines(): JSX.Element {
refetchInterval: pipelineRefetchInterval,
});
const tabItems: TabsProps['items'] = useMemo(
const tabItems: TabItemProps[] = useMemo(
() => [
{
key: 'pipelines',
@@ -83,11 +80,7 @@ function Pipelines(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<Tabs
className="pipeline-tabs"
defaultActiveKey="pipelines"
items={tabItems}
/>
<Tabs defaultValue="pipelines" items={tabItems} />
</Sentry.ErrorBoundary>
);
}

View File

@@ -340,7 +340,7 @@ function SettingsPage(): JSX.Element {
routes={routes}
activeKey={pathname}
history={history}
tabBarStyle={{ display: 'none' }}
hideTabBar
/>
</div>
</div>

View File

@@ -71,88 +71,6 @@
flex-direction: column;
gap: 25px;
padding-top: 16px;
.flamegraph-waterfall-toggle {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
height: 31px;
color: var(--l2-foreground);
padding: 5px 20px;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.ant-btn-icon {
margin-inline-end: 0px !important;
}
}
.span-list-toggle {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
height: 31px;
padding: 5px 20px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.ant-btn-icon {
margin-inline-end: 0px !important;
}
}
.trace-visualisation-tabs {
.ant-tabs-tab {
border-radius: 2px 0px 0px 0px;
background: var(--l2-background);
border-radius: 2px 2px 0px 0px;
border: 1px solid var(--l1-border);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
height: 31px;
}
.ant-tabs-tab-active {
background-color: var(--l1-background);
.ant-btn {
color: var(--l1-foreground) !important;
}
}
.ant-tabs-tab + .ant-tabs-tab {
margin: 0px;
border-left: 0px;
}
.ant-tabs-ink-bar {
height: 1px !important;
background: var(--l1-background) !important;
}
.ant-tabs-nav-list {
transform: translate(15px, 0px) !important;
}
.ant-tabs-nav::before {
border-bottom: 1px solid var(--l1-border);
}
.ant-tabs-nav {
margin: 0px;
padding: 0px !important;
}
}
}
}
}

View File

@@ -5,7 +5,7 @@ import {
ResizablePanel,
ResizablePanelGroup,
} from '@signozhq/resizable';
import { Button, Tabs } from 'antd';
import { Tabs, TabItemProps } from '@signozhq/ui/tabs';
import FlamegraphImg from 'assets/TraceDetail/Flamegraph';
import cx from 'classnames';
import TraceFlamegraph from 'container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph';
@@ -86,18 +86,11 @@ function TraceDetailsV2(): JSX.Element {
}
}, [noData]);
const items = [
const items: TabItemProps[] = [
{
label: (
<Button
type="text"
icon={<FlamegraphImg />}
className="flamegraph-waterfall-toggle"
>
Flamegraph
</Button>
),
key: 'flamegraph',
label: 'Flamegraph',
prefixIcon: <FlamegraphImg />,
children: (
<>
<TraceFlamegraph
@@ -145,11 +138,7 @@ function TraceDetailsV2(): JSX.Element {
totalSpans={traceData?.payload?.totalSpansCount || 0}
notFound={noData}
/>
{!noData ? (
<Tabs items={items} animated className="trace-visualisation-tabs" />
) : (
<NoData />
)}
{!noData ? <Tabs items={items} /> : <NoData />}
</ResizablePanel>
<ResizableHandle withHandle className="resizable-handle" />

View File

@@ -22,21 +22,6 @@ $dark-theme: 'darkMode';
&__tabs {
margin-top: 148px;
.ant-tabs {
&-nav {
&::before {
border-color: var(--l1-border);
.#{$light-theme} & {
border-color: var(--l1-border);
}
}
}
&-nav-wrap {
justify-content: center;
}
}
}
&__modal {

View File

@@ -2,7 +2,7 @@
import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import type { TabsProps } from 'antd';
import type { TabItemProps } from '@signozhq/ui/tabs';
import {
Alert,
Button,
@@ -14,8 +14,8 @@ import {
Row,
Skeleton,
Space,
Tabs,
} from 'antd';
import { Tabs } from '@signozhq/ui/tabs';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import updateCreditCardApi from 'api/v1/checkout/create';
@@ -154,7 +154,7 @@ export default function WorkspaceBlocked(): JSX.Element {
/>
));
const tabItems: TabsProps['items'] = [
const tabItems: TabItemProps[] = [
{
key: 'whyChooseSignoz',
label: t('whyChooseSignoz'),
@@ -398,8 +398,8 @@ export default function WorkspaceBlocked(): JSX.Element {
<div className="workspace-locked__tabs">
<Tabs
items={tabItems}
defaultActiveKey="youAreInGoodCompany"
onTabClick={handleTabClick}
defaultValue="youAreInGoodCompany"
onChange={handleTabClick}
/>
</div>
</>

View File

@@ -825,5 +825,6 @@ body.ai-assistant-panel-open {
// overrides
:root {
--input-focus-outline-width: 0;
--tab-list-primary-gap: 12px;
--radius-2: 4px;
}

View File

@@ -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",

4
go.mod
View File

@@ -5,9 +5,8 @@ go 1.25.7
require (
dario.cat/mergo v1.0.2
github.com/AfterShip/clickhouse-sql-parser v0.4.16
github.com/ClickHouse/clickhouse-go/v2 v2.44.0
github.com/ClickHouse/clickhouse-go/v2 v2.43.0
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/clickhouse-go-mock v0.14.0
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.144.3
github.com/antlr4-go/antlr/v4 v4.13.1
@@ -59,6 +58,7 @@ require (
github.com/smartystreets/goconvey v1.8.1
github.com/soheilhy/cmux v0.1.5
github.com/spf13/cobra v1.10.2
github.com/srikanthccv/ClickHouse-go-mock v0.13.0
github.com/stretchr/testify v1.11.1
github.com/swaggest/jsonschema-go v0.3.78
github.com/swaggest/rest v0.2.75

10
go.sum
View File

@@ -89,8 +89,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ=
github.com/ClickHouse/clickhouse-go/v2 v2.44.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c=
github.com/ClickHouse/clickhouse-go/v2 v2.43.0 h1:fUR05TrF1GyvLDa/mAQjkx7KbgwdLRffs2n9O3WobtE=
github.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g=
github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU=
github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
@@ -104,8 +104,6 @@ github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/SigNoz/clickhouse-go-mock v0.14.0 h1:yYoGMJk0UDVvLCk12dover/zoRIuznW3BfoqUhMoJSY=
github.com/SigNoz/clickhouse-go-mock v0.14.0/go.mod h1:tXmSXVLWz7N/yVLCianszmNIN4cHozXvwEXCOLzzqzI=
github.com/SigNoz/expr v1.17.7-beta h1:FyZkleM5dTQ0O6muQfwGpoH5A2ohmN/XTasRCO72gAA=
github.com/SigNoz/expr v1.17.7-beta/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkbj57eGXx8H3ZJ4zhmQXBnrW523ktj8=
@@ -1064,8 +1062,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/srikanthccv/ClickHouse-go-mock v0.12.0 h1:KUzaWTwuqMc2uf5FylM/oAcTFdE2DdZjvISm9V0/NAA=
github.com/srikanthccv/ClickHouse-go-mock v0.12.0/go.mod h1:1oUmLtXEXOyS0EEWVKlKEfLfv9y02agCMAvD3tVnhlo=
github.com/srikanthccv/ClickHouse-go-mock v0.13.0 h1:/b7DQphGkh29ocNtLh4DGmQxQYA0CfHz65Wy2zAH2GM=
github.com/srikanthccv/ClickHouse-go-mock v0.13.0/go.mod h1:LiiyBUdXNwB/1DE9rgK/8q9qjVYsTzg6WXQ/3mU3TeY=
github.com/stackitcloud/stackit-sdk-go/core v0.23.0 h1:zPrOhf3Xe47rKRs1fg/AqKYUiJJRYjdcv+3qsS50mEs=
github.com/stackitcloud/stackit-sdk-go/core v0.23.0/go.mod h1:osMglDby4csGZ5sIfhNyYq1bS1TxIdPY88+skE/kkmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View File

@@ -6,7 +6,7 @@ import (
"testing"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
cmock "github.com/SigNoz/clickhouse-go-mock"
cmock "github.com/srikanthccv/ClickHouse-go-mock"
"github.com/stretchr/testify/require"
"github.com/DATA-DOG/go-sqlmock"

View File

@@ -679,7 +679,21 @@ func (bc *bucketCache) mergeAndDeduplicateBuckets(existing, fresh []*qbtypes.Cac
// deduplicateWarnings removes duplicate warnings.
func (bc *bucketCache) deduplicateWarnings(warnings []string) []string {
return dedupeWarnings(warnings)
if len(warnings) == 0 {
return nil
}
seen := make(map[string]bool, len(warnings))
unique := make([]string, 0, len(warnings)) // Pre-allocate capacity
for _, warning := range warnings {
if !seen[warning] {
seen[warning] = true
unique = append(unique, warning)
}
}
return unique
}
// trimResultToFluxBoundary trims the result to exclude data points beyond the flux boundary.

View File

@@ -7,12 +7,6 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
)
type SkipResourceFingerprint struct {
Enabled bool `yaml:"enabled" mapstructure:"enabled"`
// If count of fingerprint is above threshold, skip the fingerprint subquery and filter on main table instead.
Threshold uint64 `yaml:"threshold" mapstructure:"threshold"`
}
// Config represents the configuration for the querier.
type Config struct {
// CacheTTL is the TTL for cached query results
@@ -21,8 +15,6 @@ type Config struct {
FluxInterval time.Duration `yaml:"flux_interval" mapstructure:"flux_interval"`
// MaxConcurrentQueries is the maximum number of concurrent queries for missing ranges
MaxConcurrentQueries int `yaml:"max_concurrent_queries" mapstructure:"max_concurrent_queries"`
// SkipResourceFingerprint configures when the resource fingerprint subquery is skipped in favor of main-table filtering.
SkipResourceFingerprint SkipResourceFingerprint `yaml:"skip_resource_fingerprint" mapstructure:"skip_resource_fingerprint"`
}
// NewConfigFactory creates a new config factory for querier.
@@ -36,10 +28,6 @@ func newConfig() factory.Config {
CacheTTL: 168 * time.Hour,
FluxInterval: 5 * time.Minute,
MaxConcurrentQueries: 4,
SkipResourceFingerprint: SkipResourceFingerprint{
Enabled: false,
Threshold: 100000,
},
}
}
@@ -54,9 +42,6 @@ func (c Config) Validate() error {
if c.MaxConcurrentQueries <= 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "max_concurrent_queries must be positive, got %v", c.MaxConcurrentQueries)
}
if c.SkipResourceFingerprint.Enabled && c.SkipResourceFingerprint.Threshold == 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "skip_resource_fingerprint.threshold must be > 0 when enabled")
}
return nil
}

View File

@@ -642,12 +642,6 @@ func (q *querier) run(
},
}
// Warnings can arrive duplicated: the bucket cache returns the cached
// portion's warnings alongside an identical warning emitted by every
// freshly-executed missing range (see mergeResults), and distinct queries
// can surface the same warning. Collapse exact duplicates before building
// the response.
warnings = dedupeWarnings(warnings)
if len(warnings) != 0 {
warns := make([]qbtypes.QueryWarnDataAdditional, len(warnings))
for i, warning := range warnings {
@@ -1131,8 +1125,6 @@ func (q *querier) adjustStepInterval(queries []qbtypes.QueryEnvelope, start, end
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
if qe.GetSource() == telemetrytypes.SourceMeter {
clampStep(qe, meterRecommended, meterMin, &warnings)
// we don't want to return warnings for meter metrics.
warnings = nil
} else {
clampStep(qe, metricRecommended, metricMin, &warnings)
}
@@ -1148,21 +1140,3 @@ func (q *querier) adjustStepInterval(queries []qbtypes.QueryEnvelope, start, end
}
return warnings
}
// dedupeWarnings removes exact-duplicate warning messages while preserving the
// order of first occurrence. Returns nil for an empty input. Warning counts are
// tiny (a handful per request), so a linear scan beats the allocation and
// hashing overhead of a map.
func dedupeWarnings(warnings []string) []string {
if len(warnings) == 0 {
return nil
}
unique := make([]string, 0, len(warnings))
// N^2 is faster than map-based deduping for small warning counts, and it preserves order of first occurrence without extra bookkeeping.
for _, warning := range warnings {
if !slices.Contains(unique, warning) {
unique = append(unique, warning)
}
}
return unique
}

View File

@@ -5,7 +5,7 @@ import (
"testing"
"time"
cmock "github.com/SigNoz/clickhouse-go-mock"
cmock "github.com/srikanthccv/ClickHouse-go-mock"
"github.com/SigNoz/signoz/pkg/flagger/flaggertest"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"

View File

@@ -88,8 +88,6 @@ func newProvider(
traceAggExprRewriter,
telemetryStore,
flagger,
cfg.SkipResourceFingerprint.Enabled,
cfg.SkipResourceFingerprint.Threshold,
)
// Create trace operator statement builder
@@ -123,9 +121,6 @@ func newProvider(
telemetrylogs.DefaultFullTextColumn,
telemetrylogs.GetBodyJSONKey,
flagger,
telemetryStore,
cfg.SkipResourceFingerprint.Enabled,
cfg.SkipResourceFingerprint.Threshold,
)
// Create audit statement builder

View File

@@ -26,7 +26,7 @@ import (
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
"github.com/SigNoz/signoz/pkg/valuer"
cmock "github.com/SigNoz/clickhouse-go-mock"
cmock "github.com/srikanthccv/ClickHouse-go-mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

View File

@@ -26,7 +26,7 @@ import (
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
"github.com/SigNoz/signoz/pkg/valuer"
cmock "github.com/SigNoz/clickhouse-go-mock"
cmock "github.com/srikanthccv/ClickHouse-go-mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

View File

@@ -25,7 +25,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
cmock "github.com/SigNoz/clickhouse-go-mock"
cmock "github.com/srikanthccv/ClickHouse-go-mock"
)
func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {

View File

@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/require"
pql "github.com/prometheus/prometheus/promql"
cmock "github.com/SigNoz/clickhouse-go-mock"
cmock "github.com/srikanthccv/ClickHouse-go-mock"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/prometheus"

View File

@@ -89,9 +89,6 @@ func prepareQuerierForLogs(t *testing.T, telemetryStore telemetrystore.Telemetry
telemetrylogs.DefaultFullTextColumn,
telemetrylogs.GetBodyJSONKey,
fl,
nil,
false,
100000,
)
return querier.New(
@@ -137,8 +134,6 @@ func prepareQuerierForTraces(t *testing.T, telemetryStore telemetrystore.Telemet
traceAggExprRewriter,
telemetryStore,
fl,
false,
100000,
)
return querier.New(

View File

@@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
cmock "github.com/SigNoz/clickhouse-go-mock"
cmock "github.com/srikanthccv/ClickHouse-go-mock"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/telemetrystore"

View File

@@ -127,7 +127,7 @@ func TestStatementBuilder(t *testing.T) {
Limit: 100,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'signoz.audit.resource.id') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'signoz.audit.resource.id') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", "019b-5678-efgh-9012", "%signoz.audit.resource.id%", "%signoz.audit.resource.id\":\"019b-5678-efgh-9012%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
},
},
@@ -144,7 +144,7 @@ func TestStatementBuilder(t *testing.T) {
Limit: 100,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_signoz$$audit$$action` = ? AND `attribute_string_signoz$$audit$$action_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_signoz$$audit$$action` = ? AND `attribute_string_signoz$$audit$$action_exists` = ?) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "delete", true, "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
},
},

View File

@@ -1205,9 +1205,6 @@ func buildJSONTestStatementBuilder(t *testing.T, addIndexes bool) (*logQueryStat
DefaultFullTextColumn,
GetBodyJSONKey,
fl,
nil,
false,
100000,
)
return statementBuilder, mockMetadataStore

View File

@@ -11,7 +11,6 @@ import (
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/querybuilder"
"github.com/SigNoz/signoz/pkg/telemetryresourcefilter"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
@@ -20,14 +19,13 @@ import (
)
type logQueryStatementBuilder struct {
logger *slog.Logger
metadataStore telemetrytypes.MetadataStore
fm qbtypes.FieldMapper
cb qbtypes.ConditionBuilder
resourceFilterResolver *telemetryresourcefilter.ResourceFingerprintResolver[qbtypes.LogAggregation]
aggExprRewriter qbtypes.AggExprRewriter
fl flagger.Flagger
skipResourceFingerprintEnabled bool
logger *slog.Logger
metadataStore telemetrytypes.MetadataStore
fm qbtypes.FieldMapper
cb qbtypes.ConditionBuilder
resourceFilterStmtBuilder qbtypes.StatementBuilder[qbtypes.LogAggregation]
aggExprRewriter qbtypes.AggExprRewriter
fl flagger.Flagger
fullTextColumn *telemetrytypes.TelemetryFieldKey
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
@@ -44,13 +42,10 @@ func NewLogQueryStatementBuilder(
fullTextColumn *telemetrytypes.TelemetryFieldKey,
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
fl flagger.Flagger,
telemetryStore telemetrystore.TelemetryStore,
skipResourceFingerprintEnable bool,
skipResourceFingerprintThreshold uint64,
) *logQueryStatementBuilder {
logsSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetrylogs")
resourceFilterResolver := telemetryresourcefilter.NewResolver[qbtypes.LogAggregation](
resourceFilterStmtBuilder := telemetryresourcefilter.New[qbtypes.LogAggregation](
settings,
DBName,
LogsResourceV2TableName,
@@ -60,21 +55,18 @@ func NewLogQueryStatementBuilder(
fullTextColumn,
jsonKeyToKey,
fl,
telemetryStore,
skipResourceFingerprintThreshold,
)
return &logQueryStatementBuilder{
logger: logsSettings.Logger(),
metadataStore: metadataStore,
fm: fieldMapper,
cb: conditionBuilder,
resourceFilterResolver: resourceFilterResolver,
aggExprRewriter: aggExprRewriter,
fl: fl,
skipResourceFingerprintEnabled: skipResourceFingerprintEnable,
fullTextColumn: fullTextColumn,
jsonKeyToKey: jsonKeyToKey,
logger: logsSettings.Logger(),
metadataStore: metadataStore,
fm: fieldMapper,
cb: conditionBuilder,
resourceFilterStmtBuilder: resourceFilterStmtBuilder,
aggExprRewriter: aggExprRewriter,
fl: fl,
fullTextColumn: fullTextColumn,
jsonKeyToKey: jsonKeyToKey,
}
}
@@ -279,11 +271,9 @@ func (b *logQueryStatementBuilder) buildListQuery(
bodyJSONEnabled = b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
)
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
if err != nil {
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
return nil, err
}
if frag != "" {
} else if frag != "" {
cteFragments = append(cteFragments, frag)
cteArgs = append(cteArgs, args)
}
@@ -325,7 +315,7 @@ func (b *logQueryStatementBuilder) buildListQuery(
sb.From(fmt.Sprintf("%s.%s", DBName, LogsV2TableName))
// Add filter conditions
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
if err != nil {
return nil, err
@@ -383,11 +373,9 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
bodyJSONEnabled = b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
)
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
if err != nil {
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
return nil, err
}
if frag != "" {
} else if frag != "" {
cteFragments = append(cteFragments, frag)
cteArgs = append(cteArgs, args)
}
@@ -431,7 +419,7 @@ func (b *logQueryStatementBuilder) buildTimeSeriesQuery(
// Add FROM clause
sb.From(fmt.Sprintf("%s.%s", DBName, LogsV2TableName))
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
if err != nil {
return nil, err
@@ -543,11 +531,9 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
bodyJSONEnabled = b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
)
frag, args, skipResourceFilter, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables)
if err != nil {
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
return nil, err
}
if frag != "" && !skipResourceCTE {
} else if frag != "" && !skipResourceCTE {
cteFragments = append(cteFragments, frag)
cteArgs = append(cteArgs, args)
}
@@ -590,7 +576,7 @@ func (b *logQueryStatementBuilder) buildScalarQuery(
sb.From(fmt.Sprintf("%s.%s", DBName, LogsV2TableName))
// Add filter conditions
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables, skipResourceFilter)
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
if err != nil {
return nil, err
@@ -654,7 +640,6 @@ func (b *logQueryStatementBuilder) addFilterCondition(
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
keys map[string][]*telemetrytypes.TelemetryFieldKey,
variables map[string]qbtypes.VariableItem,
skipResourceFilter bool,
) (querybuilder.PreparedWhereClause, error) {
var preparedWhereClause querybuilder.PreparedWhereClause
@@ -671,7 +656,7 @@ func (b *logQueryStatementBuilder) addFilterCondition(
ConditionBuilder: b.cb,
FieldKeys: keys,
BodyJSONEnabled: bodyJSONEnabled,
SkipResourceFilter: skipResourceFilter,
SkipResourceFilter: true,
FullTextColumn: b.fullTextColumn,
JsonKeyToKey: b.jsonKeyToKey,
Variables: variables,
@@ -722,30 +707,33 @@ func (b *logQueryStatementBuilder) maybeAttachResourceFilter(
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
start, end uint64,
variables map[string]qbtypes.VariableItem,
) (cteSQL string, cteArgs []any, skipResourceFilter bool, err error) {
) (cteSQL string, cteArgs []any, err error) {
if b.skipResourceFingerprintEnabled {
decision, err := b.resourceFilterResolver.Resolve(ctx, query, start, end, variables)
if err != nil {
return "", nil, true, err
}
switch decision {
case qbtypes.ResourceFilterResolveKindNoOp:
return "", nil, true, nil
case qbtypes.ResourceFilterResolveKindFallback:
return "", nil, false, nil
}
}
stmt, err := b.resourceFilterResolver.StatementBuilder().Build(
ctx, start, end, qbtypes.RequestTypeRaw, query, variables,
)
stmt, err := b.buildResourceFilterCTE(ctx, query, start, end, variables)
if err != nil {
return "", nil, true, err
return "", nil, err
}
if stmt == nil {
return "", nil, true, nil
return "", nil, nil
}
sb.Where("resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
return fmt.Sprintf("__resource_filter AS (%s)", stmt.Query), stmt.Args, true, nil
return fmt.Sprintf("__resource_filter AS (%s)", stmt.Query), stmt.Args, nil
}
func (b *logQueryStatementBuilder) buildResourceFilterCTE(
ctx context.Context,
query qbtypes.QueryBuilderQuery[qbtypes.LogAggregation],
start, end uint64,
variables map[string]qbtypes.VariableItem,
) (*qbtypes.Statement, error) {
return b.resourceFilterStmtBuilder.Build(
ctx,
start,
end,
qbtypes.RequestTypeRaw,
query,
variables,
)
}

Some files were not shown because too many files have changed in this diff Show More