mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-01 14:50:29 +01:00
Compare commits
1 Commits
issue_5131
...
fix/tag-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b90f44bba |
2
.github/workflows/build-enterprise.yaml
vendored
2
.github/workflows/build-enterprise.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/gor-signoz.yaml
vendored
2
.github/workflows/gor-signoz.yaml
vendored
@@ -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
|
||||
|
||||
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -52,7 +52,6 @@ jobs:
|
||||
- rootuser
|
||||
- serviceaccount
|
||||
- querier_json_body
|
||||
- querier_skip_resource_fingerprint
|
||||
- ttl
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
12
frontend/src/components/RouteTab/RouteTab.styles.scss
Normal file
12
frontend/src/components/RouteTab/RouteTab.styles.scss
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
.emptyIcon {
|
||||
margin-bottom: 4px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.emptyTitle {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -14,7 +14,6 @@ export interface SidebarItem {
|
||||
label?: ReactNode;
|
||||
isBeta?: boolean;
|
||||
isNew?: boolean;
|
||||
isEarlyAccess?: boolean;
|
||||
isPinned?: boolean;
|
||||
children?: SidebarItem[];
|
||||
isExternal?: boolean;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
function DashboardPageV2(): JSX.Element {
|
||||
return <>DashboardPageV2</>;
|
||||
}
|
||||
|
||||
export default DashboardPageV2;
|
||||
@@ -1,3 +1,8 @@
|
||||
import DashboardPageV2 from './DashboardPageV2';
|
||||
|
||||
function DashboardPageV2(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboard Page V2</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default DashboardPageV2;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
gap: 8px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.headerLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--muted-foreground);
|
||||
font-family: Inter;
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { AnnouncementBanner } from '@signozhq/ui/announcement-banner';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { LayoutGrid } from '@signozhq/icons';
|
||||
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import DashboardsList from './components/DashboardsList';
|
||||
|
||||
import styles from './DashboardsListPageV2.module.scss';
|
||||
|
||||
function DashboardsListPageV2(): JSX.Element {
|
||||
const [showBanner, setShowBanner] = useState(true);
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
{showBanner && (
|
||||
<AnnouncementBanner
|
||||
type="warning"
|
||||
onClose={(): void => setShowBanner(false)}
|
||||
>
|
||||
You're on the V2 dashboards page. If you landed here unintentionally,
|
||||
please reach out to Ashwin.
|
||||
</AnnouncementBanner>
|
||||
)}
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerLeft}>
|
||||
<LayoutGrid size={14} className={styles.icon} />
|
||||
<Typography.Text className={styles.text}>Dashboards</Typography.Text>
|
||||
</div>
|
||||
<HeaderRightSection
|
||||
enableAnnouncements={false}
|
||||
enableShare
|
||||
enableFeedback
|
||||
/>
|
||||
</div>
|
||||
<DashboardsList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardsListPageV2;
|
||||
@@ -1,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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,52 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
export interface DashboardDynamicColumns {
|
||||
createdAt: boolean;
|
||||
createdBy: boolean;
|
||||
updatedAt: boolean;
|
||||
updatedBy: boolean;
|
||||
}
|
||||
|
||||
export enum DynamicColumns {
|
||||
CREATED_AT = 'createdAt',
|
||||
CREATED_BY = 'createdBy',
|
||||
UPDATED_AT = 'updatedAt',
|
||||
UPDATED_BY = 'updatedBy',
|
||||
}
|
||||
|
||||
const DEFAULT_COLUMNS: DashboardDynamicColumns = {
|
||||
createdAt: true,
|
||||
createdBy: true,
|
||||
updatedAt: false,
|
||||
updatedBy: false,
|
||||
};
|
||||
|
||||
interface DashboardsListVisibleColumnsState {
|
||||
visibleColumns: DashboardDynamicColumns;
|
||||
setVisibleColumns: (next: DashboardDynamicColumns) => void;
|
||||
}
|
||||
|
||||
export const useDashboardsListVisibleColumnsStore =
|
||||
create<DashboardsListVisibleColumnsState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
visibleColumns: DEFAULT_COLUMNS,
|
||||
setVisibleColumns: (next): void => {
|
||||
set({ visibleColumns: next });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: LOCALSTORAGE.DASHBOARDS_LIST_VISIBLE_COLUMNS,
|
||||
merge: (persisted, current) => ({
|
||||
...current,
|
||||
visibleColumns: {
|
||||
...DEFAULT_COLUMNS,
|
||||
...((persisted as Partial<DashboardsListVisibleColumnsState>)
|
||||
?.visibleColumns ?? {}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -1,34 +0,0 @@
|
||||
.menuItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.templatesItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.primaryButton {
|
||||
padding: 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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,96 +0,0 @@
|
||||
.container {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.viewContent {
|
||||
width: calc(100% - 30px);
|
||||
max-width: 836px;
|
||||
|
||||
:global(.ant-table-wrapper) :global(.ant-table-cell) {
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
background: var(--l1-background) !important;
|
||||
}
|
||||
|
||||
:global(.ant-table-wrapper)
|
||||
:global(.ant-table-tbody)
|
||||
:global(.ant-table-row)
|
||||
:global(.ant-table-cell)
|
||||
> div {
|
||||
// Row content is the only child of the td; it carries the borders.
|
||||
}
|
||||
|
||||
:global(.ant-table-wrapper)
|
||||
:global(.ant-table-tbody)
|
||||
:global(.ant-table-row:last-child)
|
||||
:global(.ant-table-cell)
|
||||
> div {
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item) > a {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item-active) {
|
||||
background-color: var(--primary-background);
|
||||
}
|
||||
|
||||
:global(.ant-pagination-item-active) > a {
|
||||
color: var(--foreground) !important;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-lg);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 28px;
|
||||
letter-spacing: -0.09px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--font-size-sm);
|
||||
font-style: normal;
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
.integrationsContainer {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.integrationsContent {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
@@ -1,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;
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Table } from 'antd';
|
||||
import type { TableProps } from 'antd/lib';
|
||||
|
||||
import type { DashboardListItem } from '../../utils';
|
||||
import DashboardRow from '../DashboardRow/DashboardRow';
|
||||
|
||||
interface Props {
|
||||
dashboards: DashboardListItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
onPageChange: (page: number) => void;
|
||||
canAct: boolean;
|
||||
showUpdatedAt: boolean;
|
||||
showUpdatedBy: boolean;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
function DashboardsListContent({
|
||||
dashboards,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
onPageChange,
|
||||
canAct,
|
||||
showUpdatedAt,
|
||||
showUpdatedBy,
|
||||
loading,
|
||||
}: Props): JSX.Element {
|
||||
const columns: TableProps<DashboardListItem>['columns'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'Dashboards',
|
||||
key: 'dashboard',
|
||||
render: (_, dashboard, index): JSX.Element => (
|
||||
<DashboardRow
|
||||
dashboard={dashboard}
|
||||
index={index}
|
||||
canAct={canAct}
|
||||
showUpdatedAt={showUpdatedAt}
|
||||
showUpdatedBy={showUpdatedBy}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[canAct, showUpdatedAt, showUpdatedBy],
|
||||
);
|
||||
|
||||
const paginationConfig = total > pageSize && {
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
onChange: onPageChange,
|
||||
current: page,
|
||||
total,
|
||||
hideOnSinglePage: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dashboards.map((d) => ({ ...d, key: d.id }))}
|
||||
showSorterTooltip
|
||||
loading={loading}
|
||||
showHeader={false}
|
||||
pagination={paginationConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardsListContent;
|
||||
@@ -1,3 +0,0 @@
|
||||
import DashboardsList from './DashboardsList';
|
||||
|
||||
export default DashboardsList;
|
||||
@@ -1,73 +0,0 @@
|
||||
.contentContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.contentHeader {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.jsonError {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
color: var(--warning-background);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:global(.importJsonModalWrapper) {
|
||||
:global(.ant-modal-content) {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.margin) {
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
:global(.view-lines) {
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
:global(.ant-modal-footer) {
|
||||
margin-top: 0;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
@@ -1,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')}
|
||||
<ExternalLink size={14} />
|
||||
</Button>
|
||||
</a>
|
||||
</Flex>
|
||||
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
loading={isCreating}
|
||||
className="periscope-btn primary"
|
||||
type="primary"
|
||||
>
|
||||
{t('import_and_next')} <MoveRight size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={styles.contentContainer}>
|
||||
<div className={styles.contentHeader}>
|
||||
<Typography.Text>{t('import_json')}</Typography.Text>
|
||||
</div>
|
||||
<MEditor
|
||||
language="json"
|
||||
height="40vh"
|
||||
onChange={(newValue): void => setEditorValue(newValue || '')}
|
||||
value={editorValue}
|
||||
options={{
|
||||
scrollbar: { alwaysConsumeMouseWheel: false },
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
fontFamily: 'Space Mono',
|
||||
}}
|
||||
theme={isDarkMode ? 'my-theme' : 'light'}
|
||||
onMount={(_, monaco): void => {
|
||||
document.fonts.ready.then(() => {
|
||||
monaco.editor.remeasureFonts();
|
||||
});
|
||||
}}
|
||||
beforeMount={setEditorTheme}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportJSONModal;
|
||||
@@ -1,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,
|
||||
};
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
{
|
||||
"display": {
|
||||
"name": "NV dashboard with sections",
|
||||
"description": ""
|
||||
},
|
||||
"datasources": {
|
||||
"SigNozDatasource": {
|
||||
"default": true,
|
||||
"plugin": {
|
||||
"kind": "signoz/Datasource",
|
||||
"spec": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"panels": {
|
||||
"b424e23b": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": ""
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/NumberPanel",
|
||||
"spec": {
|
||||
"formatting": {
|
||||
"unit": "s",
|
||||
"decimalPrecision": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "container.cpu.time",
|
||||
"reduceTo": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"timeAggregation": "rate"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"251df4d5": {
|
||||
"kind": "Panel",
|
||||
"spec": {
|
||||
"display": {
|
||||
"name": ""
|
||||
},
|
||||
"plugin": {
|
||||
"kind": "signoz/TimeSeriesPanel",
|
||||
"spec": {
|
||||
"visualization": {
|
||||
"fillSpans": false
|
||||
},
|
||||
"formatting": {
|
||||
"unit": "recommendations",
|
||||
"decimalPrecision": "2"
|
||||
},
|
||||
"chartAppearance": {
|
||||
"lineInterpolation": "spline",
|
||||
"showPoints": false,
|
||||
"lineStyle": "solid",
|
||||
"fillMode": "none",
|
||||
"spanGaps": {"fillOnlyBelow": true}
|
||||
},
|
||||
"legend": {
|
||||
"position": "bottom"
|
||||
}
|
||||
}
|
||||
},
|
||||
"queries": [
|
||||
{
|
||||
"kind": "TimeSeriesQuery",
|
||||
"spec": {
|
||||
"plugin": {
|
||||
"kind": "signoz/BuilderQuery",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"aggregations": [
|
||||
{
|
||||
"metricName": "app_recommendations_counter",
|
||||
"reduceTo": "sum",
|
||||
"spaceAggregation": "sum",
|
||||
"timeAggregation": "rate"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"expression": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"layouts": [
|
||||
{
|
||||
"kind": "Grid",
|
||||
"spec": {
|
||||
"display": {
|
||||
"title": "Bravo"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/b424e23b"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Grid",
|
||||
"spec": {
|
||||
"display": {
|
||||
"title": "Alpha"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"width": 6,
|
||||
"height": 6,
|
||||
"content": {
|
||||
"$ref": "#/spec/panels/251df4d5"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,11 +0,0 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
height: 125px;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Skeleton } from 'antd';
|
||||
|
||||
import styles from './LoadingState.module.scss';
|
||||
|
||||
function LoadingState(): JSX.Element {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Skeleton.Input active size="large" className={styles.skeleton} />
|
||||
<Skeleton.Input active size="large" className={styles.skeleton} />
|
||||
<Skeleton.Input active size="large" className={styles.skeleton} />
|
||||
<Skeleton.Input active size="large" className={styles.skeleton} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadingState;
|
||||
@@ -1,5 +0,0 @@
|
||||
.wrapper {
|
||||
composes: cardWrapper from '../states.module.scss';
|
||||
padding: 105px 190px;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
|
||||
|
||||
import styles from './NoResultsState.module.scss';
|
||||
|
||||
interface Props {
|
||||
searchString: string;
|
||||
}
|
||||
|
||||
function NoResultsState({ searchString }: Props): JSX.Element {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<img src={emptyStateUrl} alt="img" height={32} width={32} />
|
||||
<Typography.Text>
|
||||
No dashboards found for {searchString}. Create a new dashboard?
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoResultsState;
|
||||
@@ -1,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;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import {
|
||||
parseAsInteger,
|
||||
parseAsString,
|
||||
parseAsStringLiteral,
|
||||
useQueryState,
|
||||
type Options,
|
||||
type UseQueryStateReturn,
|
||||
} from 'nuqs';
|
||||
|
||||
export const SORT_COLUMNS = ['updated_at', 'created_at', 'name'] as const;
|
||||
export type SortColumn = (typeof SORT_COLUMNS)[number];
|
||||
|
||||
export const SORT_ORDERS = ['asc', 'desc'] as const;
|
||||
export type SortOrder = (typeof SORT_ORDERS)[number];
|
||||
|
||||
const opts: Options = { history: 'push' };
|
||||
|
||||
export const useSortColumn = (): UseQueryStateReturn<SortColumn, SortColumn> =>
|
||||
useQueryState(
|
||||
'sort',
|
||||
parseAsStringLiteral(SORT_COLUMNS)
|
||||
.withDefault('updated_at')
|
||||
.withOptions(opts),
|
||||
);
|
||||
|
||||
export const useSortOrder = (): UseQueryStateReturn<SortOrder, SortOrder> =>
|
||||
useQueryState(
|
||||
'order',
|
||||
parseAsStringLiteral(SORT_ORDERS).withDefault('desc').withOptions(opts),
|
||||
);
|
||||
|
||||
export const usePage = (): UseQueryStateReturn<number, number> =>
|
||||
useQueryState('page', parseAsInteger.withDefault(1).withOptions(opts));
|
||||
|
||||
export const useSearch = (): UseQueryStateReturn<string, string> =>
|
||||
useQueryState('search', parseAsString.withDefault('').withOptions(opts));
|
||||
@@ -1,3 +1,9 @@
|
||||
import DashboardsListPageV2 from './DashboardsListPageV2';
|
||||
function DashboardsListPageV2(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<h1>Dashboards List Page V2</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardsListPageV2;
|
||||
|
||||
@@ -1,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();
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.pipeline-tabs {
|
||||
.ant-tabs-content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.ant-tabs-tabpane-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ function SettingsPage(): JSX.Element {
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
tabBarStyle={{ display: 'none' }}
|
||||
hideTabBar
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -825,5 +825,6 @@ body.ai-assistant-panel-open {
|
||||
// overrides
|
||||
:root {
|
||||
--input-focus-outline-width: 0;
|
||||
--tab-list-primary-gap: 12px;
|
||||
--radius-2: 4px;
|
||||
}
|
||||
|
||||
@@ -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
4
go.mod
@@ -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
10
go.sum
@@ -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=
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1205,9 +1205,6 @@ func buildJSONTestStatementBuilder(t *testing.T, addIndexes bool) (*logQueryStat
|
||||
DefaultFullTextColumn,
|
||||
GetBodyJSONKey,
|
||||
fl,
|
||||
nil,
|
||||
false,
|
||||
100000,
|
||||
)
|
||||
|
||||
return statementBuilder, mockMetadataStore
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user