Compare commits

...

9 Commits

Author SHA1 Message Date
Nikhil Soni
da050072a5 test(traces): expect scope in the all-fields list view
Assisted-by: Claude Opus 5.5
2026-10-28 22:51:04 +05:30
Nikhil Soni
b7cedc9ffd refactor(traces): decode declared JSON sub-paths flat in JSONValue
Keeps the querier free of a scope-specific case, so any JSON column with a declared JSON sub-path (a future logs scope) gets the same shape.

Assisted-by: Claude Opus 5.5
2026-10-28 22:51:04 +05:30
Nikhil Soni
450882ac8d Revert "feat(tracedetail): show span scope in the span details drawer"
This reverts commit 293b58f7f8.
2026-10-28 22:18:00 +05:30
Nikhil Soni
293b58f7f8 feat(tracedetail): show span scope in the span details drawer
Assisted-by: Claude Opus 5.5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:17:15 +05:30
Nikhil Soni
76edf09cbe chore(api): regenerate frontend client for span scope
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:17:14 +05:30
Nikhil Soni
80692907c9 chore(openapi): regenerate spec for span scope
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:17:14 +05:30
Nikhil Soni
27804e8fca feat(traces): return instrumentation scope in waterfall, flamegraph and list view
Scope attributes stay flat dotted keys, matching span attributes.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:17:14 +05:30
Aditya Singh
5b5b9794f9 chore(saved-views): retire the old views pages, v1 client and the flag (#13258)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
cacheci / tests (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
#### Description
- The new saved views panel and the bottom strip are on for everyone.
This PR removes the old saved views code they replaced:
- the Views tabs and the saved views page on Logs, Traces, Metrics and
Meter
  - the `ExplorerCard` and `ExplorerOptions` bars
  - the v1 saved views client
  - the `SAVED_VIEW_ENABLED` flag
- Old views URLs still redirect to their explorer.
- The explorer URL now carries only `viewKey`. The app no longer writes
`viewName`.
- `ExplorerCard` no longer runs. Its header was hidden, but it still
fetched the v1 views list on every Logs, Traces and LLM explorer load.
- Stories now mock `/api/v2/saved_views`, and the old Views tab stories
are gone.

#### Issues closed by this PR

Closes https://github.com/SigNoz/events-pod/issues/77

#### Additional Information
- The dashboards list status bar and the side nav Noz entry stay. Both
are still live UI, and the status bar holds the only rail toggle.
- Noz no longer gets the view name in its page context. It still gets
the view id.
2026-10-28 14:30:39 +00:00
Jugal Kishore
1957e5a112 feat(onboarding): add Hono, Caddy, PM2, and GCP one-click datasources (#13291)
#### Description

- Add onboarding cards for Hono, Caddy, PM2, and GCP One-Click
Integration.
- GCP Integration card: add `logs` tag and log keywords, and route to
Integrations after onboarding.
- Add Hono, Caddy, and PM2 logos.

#### Issues closed by this PR

Closes SigNoz/signoz.io#4390
Closes SigNoz/signoz.io#4387
Closes SigNoz/signoz.io#4370
Closes SigNoz/signoz.io#4367
Closes SigNoz/signoz.io#4356
2026-10-28 13:42:02 +00:00
159 changed files with 588 additions and 5480 deletions

View File

@@ -46,9 +46,9 @@ One endpoint feeding several widgets stays one handler that reads the request. `
```ts
rest.get(
'http://localhost/api/v1/explorer/views',
'http://localhost/api/v2/saved_views',
response.json((req) =>
savedViews(values.savedViews, req.url.searchParams.get('sourcePage') ?? 'logs'),
savedViews(values.savedViews, req.url.searchParams.get('source') ?? 'logs'),
),
),
```

View File

@@ -12127,6 +12127,8 @@ components:
additionalProperties:
type: string
type: object
scope:
$ref: '#/components/schemas/SpantypesScope'
spanId:
type: string
timestamp:
@@ -12143,6 +12145,7 @@ components:
- event
- attributes
- resource
- scope
type: object
SpantypesGettableFlamegraphTrace:
properties:
@@ -12383,6 +12386,20 @@ components:
nullable: true
type: array
type: object
SpantypesScope:
properties:
attributes:
additionalProperties: {}
type: object
name:
type: string
version:
type: string
required:
- name
- version
- attributes
type: object
SpantypesSpanAggregation:
properties:
aggregation:
@@ -12752,6 +12769,8 @@ components:
type: object
response_status_code:
type: string
scope:
$ref: '#/components/schemas/SpantypesScope'
span_id:
type: string
status_code:

View File

@@ -141,7 +141,7 @@ const preview: Preview = {
'Services',
['List', 'Detail', 'Top Level Operations', 'Service Map'],
'Logs',
['Explorer', 'Live Tail', 'Saved Views', 'Pipelines', 'Settings'],
['Explorer', 'Live Tail', 'Pipelines', 'Settings'],
'Traces',
['Explorer', 'Trace Details', 'Funnel Details'],
'Metrics',

View File

@@ -1,3 +0,0 @@
{
"name_of_the_view": "Name of the view"
}

View File

@@ -47,7 +47,6 @@
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
"METER": "SigNoz | Meter",
"ROLES_SETTINGS": "SigNoz | Roles",
"MEMBERS_SETTINGS": "SigNoz | Members",

View File

@@ -1,4 +0,0 @@
{
"name_of_the_view": "Name of the view",
"delete_confirm_message": "Are you sure you want to delete {{viewName}} view? Deleting a view is irreversible and cannot be undone."
}

View File

@@ -50,8 +50,6 @@
"WORKSPACE_SUSPENDED": "SigNoz | Workspace Suspended",
"WORKSPACE_ACCESS_RESTRICTED": "SigNoz | Workspace Access Restricted",
"SUPPORT": "SigNoz | Support",
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"TRACES_FUNNELS": "SigNoz | Traces Funnels",
"DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts",
@@ -66,10 +64,8 @@
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
"API_MONITORING": "SigNoz | External APIs",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
"METER": "SigNoz | Meter",
"ROLES_SETTINGS": "SigNoz | Roles",
"MEMBERS_SETTINGS": "SigNoz | Members",

View File

@@ -303,10 +303,10 @@ describe('PrivateRoute', () => {
it.each([
['/logs-save-views', ROUTES.LOGS_EXPLORER],
['/traces-save-views', ROUTES.TRACES_EXPLORER],
[ROUTES.LOGS_SAVE_VIEWS, ROUTES.LOGS_EXPLORER],
[ROUTES.TRACES_SAVE_VIEWS, ROUTES.TRACES_EXPLORER],
[ROUTES.METRICS_EXPLORER_VIEWS, ROUTES.METRICS_EXPLORER_EXPLORER],
[ROUTES.METER_EXPLORER_VIEWS, ROUTES.METER_EXPLORER],
['/logs/saved-views', ROUTES.LOGS_EXPLORER],
['/traces/saved-views', ROUTES.TRACES_EXPLORER],
['/metrics-explorer/views', ROUTES.METRICS_EXPLORER_EXPLORER],
['/meter/explorer/views', ROUTES.METER_EXPLORER],
])('should redirect the old views route %s to %s', (from, to) => {
renderPrivateRoute({ initialRoute: from });

View File

@@ -18,7 +18,6 @@ import AppLayout from 'container/AppLayout';
import Hex from 'crypto-js/enc-hex';
import HmacSHA256 from 'crypto-js/hmac-sha256';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import { ChatSupportState, useChatSupport } from 'hooks/useChatSupport';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
@@ -66,7 +65,6 @@ function App(): JSX.Element {
} = useAppContext();
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isSavedViewEnabled = useSavedViewEnabled();
const chatSupport = useChatSupport();
const { hostname } = window.location;
@@ -256,18 +254,8 @@ function App(): JSX.Element {
useEffect(() => {
// The bottom strip carries Support, so the floating bubble goes entirely.
if (
isSavedViewEnabled ||
pathname === ROUTES.ONBOARDING ||
pathname.startsWith('/public/dashboard/') ||
pathname === '/ai-assistant' ||
pathname.startsWith('/ai-assistant/')
) {
window.Pylon?.('hideChatBubble');
} else {
window.Pylon?.('showChatBubble');
}
}, [pathname, isSavedViewEnabled]);
window.Pylon?.('hideChatBubble');
}, [pathname]);
// Identity for the Pylon widget. Whether this user gets Pylon at all is
// `useChatSupport`'s call — this only fills in who they are.

View File

@@ -26,10 +26,6 @@ export const ServiceMapPage = Loadable(
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
);
export const LogsSaveViews = Loadable(
() => import(/* webpackChunkName: "LogsSaveViews" */ 'pages/LogsModulePage'), // TODO: Add a wrapper so that the same component can be used in traces
);
export const TracesExplorer = Loadable(
() =>
import(
@@ -37,11 +33,6 @@ export const TracesExplorer = Loadable(
),
);
export const TracesSaveViews = Loadable(
() =>
import(/* webpackChunkName: "Traces Save Views" */ 'pages/TracesModulePage'),
);
export const TracesFunnels = Loadable(
() =>
import(/* webpackChunkName: "Traces Funnels" */ 'pages/TracesModulePage'),

View File

@@ -26,7 +26,6 @@ import {
Login,
Logs,
LogsIndexToFields,
LogsSaveViews,
MessagingQueuesMainPage,
MeterExplorerPage,
MetricsExplorer,
@@ -48,7 +47,6 @@ import {
TracesExplorer,
TracesFunnelDetails,
TracesFunnels,
TracesSaveViews,
UnAuthorized,
UsageExplorerPage,
WorkspaceAccessRestricted,
@@ -120,13 +118,6 @@ const routes: AppRoutes[] = [
exact: true,
key: 'SERVICE_MAP',
},
{
path: ROUTES.LOGS_SAVE_VIEWS,
component: LogsSaveViews,
isPrivate: true,
exact: true,
key: 'LOGS_SAVE_VIEWS',
},
{
path: ROUTES.TRACE_DETAIL,
exact: true,
@@ -218,13 +209,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'TRACES_EXPLORER',
},
{
path: ROUTES.TRACES_SAVE_VIEWS,
exact: true,
component: TracesSaveViews,
isPrivate: true,
key: 'TRACES_SAVE_VIEWS',
},
{
path: ROUTES.TRACES_FUNNELS,
exact: true,
@@ -414,13 +398,6 @@ const routes: AppRoutes[] = [
key: 'METRICS_EXPLORER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER_VIEWS,
exact: true,
component: MetricsExplorer,
key: 'METRICS_EXPLORER_VIEWS',
isPrivate: true,
},
{
path: ROUTES.METRICS_EXPLORER_VOLUME_CONTROL,
exact: true,
@@ -443,13 +420,6 @@ const routes: AppRoutes[] = [
key: 'METER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_VIEWS,
exact: true,
component: MeterExplorerPage,
key: 'METER_EXPLORER_VIEWS',
isPrivate: true,
},
{
path: ROUTES.API_MONITORING,
exact: true,

View File

@@ -13250,6 +13250,23 @@ export type SpantypesFlamegraphSpanDTOAttributes = { [key: string]: unknown };
export type SpantypesFlamegraphSpanDTOResource = { [key: string]: string };
export type SpantypesScopeDTOAttributes = { [key: string]: unknown };
export interface SpantypesScopeDTO {
/**
* @type object
*/
attributes: SpantypesScopeDTOAttributes;
/**
* @type string
*/
name: string;
/**
* @type string
*/
version: string;
}
export interface SpantypesFlamegraphSpanDTO {
/**
* @type object
@@ -13285,6 +13302,7 @@ export interface SpantypesFlamegraphSpanDTO {
* @type object
*/
resource: SpantypesFlamegraphSpanDTOResource;
scope: SpantypesScopeDTO;
/**
* @type string
*/
@@ -13830,6 +13848,7 @@ export interface SpantypesWaterfallSpanDTO {
* @type string
*/
response_status_code?: string;
scope?: SpantypesScopeDTO;
/**
* @type string
*/

View File

@@ -1,5 +0,0 @@
import axios from 'api';
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
export const deleteView = (uuid: string): Promise<DeleteViewPayloadProps> =>
axios.delete(`/explorer/views/${uuid}`);

View File

@@ -1,9 +0,0 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { AllViewsProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
export const getAllViews = (
sourcepage: DataSource | 'meter',
): Promise<AxiosResponse<AllViewsProps>> =>
axios.get(`/explorer/views?sourcePage=${sourcepage}`);

View File

@@ -1,24 +0,0 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { ViewProps } from 'types/api/saveViews/types';
/**
* Fetches a single saved view by ID (`GET /api/v1/explorer/views/{viewId}`).
*
* Hand-maintained alongside the other `api/saveView/*` clients — explorer views
* are not in `docs/api/openapi.yml`, so Orval does not generate a hook here
* (unlike e.g. `useGetChannelByID` under `api/generated/services/channels`).
*
* Used by the AI assistant "Open view" action to load `compositeQuery` and
* navigate to the correct explorer without listing every view per source page.
* See `container/AIAssistant/components/ActionsSection/utils/openSavedView.ts`.
*/
export interface GetViewByIdProps {
status: string;
data: ViewProps;
}
export const getViewById = (
viewKey: string,
): Promise<AxiosResponse<GetViewByIdProps>> =>
axios.get(`/explorer/views/${viewKey}`);

View File

@@ -1,16 +0,0 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
export const saveView = ({
compositeQuery,
sourcePage,
viewName,
extraData,
}: SaveViewProps): Promise<AxiosResponse<SaveViewPayloadProps>> =>
axios.post('/explorer/views', {
name: viewName,
sourcePage,
compositeQuery,
extraData,
});

View File

@@ -1,19 +0,0 @@
import axios from 'api';
import {
UpdateViewPayloadProps,
UpdateViewProps,
} from 'types/api/saveViews/types';
export const updateView = ({
compositeQuery,
viewName,
extraData,
sourcePage,
viewKey,
}: UpdateViewProps): Promise<UpdateViewPayloadProps> =>
axios.put(`/explorer/views/${viewKey}`, {
name: viewName,
compositeQuery,
extraData,
sourcePage,
});

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#1F88C0" d="M11.094.47c-.842 0-1.696.092-2.552.288a11.37 11.37 0 0 0-4.87 2.423 10.632 10.632 0 0 0-2.36 2.826A10.132 10.132 0 0 0 .305 8.582c-.398 1.62-.4 3.336-.043 5.048.085.405.183.809.31 1.212a11.85 11.85 0 0 0 1.662 3.729 3.273 3.273 0 0 0-.086.427 3.323 3.323 0 0 0 2.848 3.71 3.279 3.279 0 0 0 1.947-.346c1.045.51 2.17.864 3.339 1.04a11.66 11.66 0 0 0 4.285-.155 11.566 11.566 0 0 0 4.936-2.485 10.643 10.643 0 0 0 2.352-2.894 11.164 11.164 0 0 0 1.356-4.424 11.214 11.214 0 0 0-.498-4.335c.175-.077.338-.175.486-.293a.444.444 89.992 0 0 .001 0c.402-.322.693-.794.777-1.342a2.146 2.146 0 0 0-1.79-2.434 2.115 2.115 0 0 0-1.205.171c-.038-.043-.078-.086-.113-.13a11.693 11.693 0 0 0-3.476-2.93 13.348 13.348 0 0 0-1.76-.81 13.55 13.55 0 0 0-2.06-.613A12.121 12.121 0 0 0 11.093.47Zm.714.328c.345-.004.688.01 1.028.042a9.892 9.892 0 0 1 2.743.639c.984.39 1.89.958 2.707 1.632.803.662 1.502 1.45 2.091 2.328.026.039.048.08.07.12a2.12 2.12 0 0 0-.435 2.646c-.158.114-.97.692-1.634 1.183-.414.308-.733.557-.733.557l.581.68s.296-.276.665-.638c.572-.562 1.229-1.233 1.395-1.403a2.122 2.122 0 0 0 1.907.677 11.229 11.229 0 0 1-.013 4.046 11.41 11.41 0 0 1-1.475 3.897 12.343 12.343 0 0 1-2.079 2.587c-1.19 1.125-2.633 2.022-4.306 2.531a10.826 10.826 0 0 1-3.973.484 11.04 11.04 0 0 1-3.057-.652 3.304 3.304 0 0 0 1.417-2.294 3.275 3.275 0 0 0-.294-1.842c.18-.162.403-.363.656-.6 1.015-.955 2.353-2.303 2.353-2.303l-.47-.599s-1.63.972-2.801 1.728c-.307.198-.573.378-.777.517a3.273 3.273 0 0 0-1.516-.611c-1.507-.198-2.927.672-3.487 2.017a10.323 10.323 0 0 1-.695-1.078A10.92 10.92 0 0 1 .728 14.8a10.35 10.35 0 0 1-.2-1.212c-.164-1.653.103-3.258.629-4.754a12.95 12.95 0 0 1 1.087-2.288c.57-.968 1.248-1.872 2.069-2.656A11.013 11.013 0 0 1 11.808.797Zm-.147 3.257a3.838 3.838 0 0 0-3.82 3.82v2.36h-.94c-.751 0-1.377.625-1.377 1.377v3.8h1.46v-3.718h9.354v6.264H10.02v1.46h6.4c.751 0 1.377-.625 1.377-1.377v-6.43c0-.751-.626-1.377-1.377-1.377h-.94v-2.36a3.838 3.838 0 0 0-3.82-3.819zm0 1.46a2.371 2.371 0 0 1 2.36 2.36v2.36H9.3v-2.36a2.372 2.372 0 0 1 2.36-2.36zm10.141.392a1.253 1.253 0 0 1 1.296 1.434c-.049.319-.217.59-.453.78-.266.213-.61.318-.968.264a1.253 1.253 0 0 1-1.045-1.42 1.255 1.255 0 0 1 1.17-1.058zM5.384 17.425a2.02 2.02 0 0 1 1.917 1.298c.116.3.159.628.114.967a2.015 2.015 0 0 1-2.249 1.728 2.016 2.016 0 0 1-1.727-2.25 2.017 2.017 0 0 1 1.945-1.743z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#E36002" d="M12.445.002a45.529 45.529 0 0 0-5.252 8.146 8.595 8.595 0 0 1-.555-.53 27.796 27.796 0 0 0-1.205-1.542 8.762 8.762 0 0 0-1.251 2.12 20.743 20.743 0 0 0-1.448 5.88 8.867 8.867 0 0 0 .338 3.468c1.312 3.48 3.794 5.593 7.445 6.337 3.055.438 5.755-.333 8.097-2.312 2.677-2.59 3.359-5.634 2.047-9.132a33.287 33.287 0 0 0-2.988-5.59A91.34 91.34 0 0 0 12.615.053a.216.216 0 0 0-.17-.051Zm-.336 3.906a50.93 50.93 0 0 1 4.794 6.552c.448.767.817 1.57 1.108 2.41.606 2.386-.044 4.354-1.951 5.904-1.845 1.298-3.87 1.683-6.072 1.156-2.376-.737-3.75-2.335-4.121-4.794a5.107 5.107 0 0 1 .242-2.266c.358-.908.79-1.774 1.3-2.601l1.446-2.121a397.33 397.33 0 0 0 3.254-4.24Z"/></svg>

After

Width:  |  Height:  |  Size: 758 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -1,43 +0,0 @@
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Button } from 'antd';
import AddCreditCardModal from 'components/AddCreditCardModal/AddCreditCardModal';
import logEvent from 'api/common/logEvent';
import { MessageSquareText } from '@signozhq/icons';
export default function ChatSupportGateway(): JSX.Element {
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] =
useState(false);
const { pathname } = useLocation();
return (
<>
<div className="chat-support-gateway">
<Button
className="chat-support-gateway-btn"
onClick={(): void => {
logEvent('Disabled Chat Support: Clicked', {
source: `chat support icon`,
page: pathname,
});
setIsAddCreditCardModalOpen(true);
}}
>
<MessageSquareText size={24} />
</Button>
</div>
<AddCreditCardModal
open={isAddCreditCardModalOpen}
onClose={(): void => setIsAddCreditCardModalOpen(false)}
onAddCreditCard={(): void => {
logEvent('Add Credit card modal: Clicked', {
source: `chat support icon`,
page: pathname,
});
}}
/>
</>
);
}

View File

@@ -1,252 +0,0 @@
import { useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Button, Col, Popover, Row, Select, Space } from 'antd';
import { Dropdown, type DropdownItemType } from '@signozhq/ui/dropdown';
import { Typography } from '@signozhq/ui/typography';
import axios from 'axios';
import TextToolTip from 'components/TextToolTip';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
import { useUpdateView } from 'hooks/saveViews/useUpdateView';
import useErrorNotification from 'hooks/useErrorNotification';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { ExploreHeaderToolTip, SaveButtonText } from './constants';
import MenuItemGenerator from './MenuItemGenerator';
import SaveViewWithName from './SaveViewWithName';
import {
DropDownOverlay,
ExplorerCardHeadContainer,
OffSetCol,
} from './styles';
import { ExplorerCardProps } from './types';
import { deleteViewHandler } from './utils';
import { Ellipsis, Save, Share2, Trash2 } from '@signozhq/icons';
function ExplorerCard({
sourcepage,
children,
}: ExplorerCardProps): JSX.Element {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [, setCopyUrl] = useCopyToClipboard();
const { notifications } = useNotifications();
const onCopyUrlHandler = (): void => {
setCopyUrl(window.location.href);
notifications.success({
message: 'Copied to clipboard',
});
};
const {
currentQuery,
panelType,
redirectWithQueryBuilderData,
updateAllQueriesOperators,
isStagedQueryUpdated,
} = useQueryBuilder();
const {
data: viewsData,
isLoading,
error,
isRefetching,
refetch: refetchAllView,
} = useGetAllViews(sourcepage);
useErrorNotification(error);
const handleOpenChange = (newOpen = false): void => {
setIsOpen(newOpen);
};
const { viewName, viewKey } = useGetSavedViewParams();
const { options } = useOptionsMenu({
storageKey:
sourcepage === DataSource.TRACES
? LOCALSTORAGE.TRACES_LIST_OPTIONS
: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: sourcepage,
aggregateOperator: StringOperators.NOOP,
});
const isQueryUpdated = isStagedQueryUpdated(
viewsData?.data?.data,
viewKey,
options,
);
const { mutateAsync: updateViewAsync } = useUpdateView({
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: '',
sourcePage: sourcepage,
viewName,
});
const { mutateAsync: deleteViewAsync } = useDeleteView(viewKey);
const showErrorNotification = (err: Error): void => {
notifications.error({
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
});
};
const onDeleteHandler = (): void =>
deleteViewHandler({
deleteViewAsync,
notifications,
panelType,
redirectWithQueryBuilderData,
refetchAllView,
viewId: viewKey,
viewKey,
updateAllQueriesOperators,
sourcePage: sourcepage,
});
const onUpdateQueryHandler = (): void => {
updateViewAsync(
{
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: '',
sourcePage: sourcepage,
viewName,
},
{
onSuccess: () => {
notifications.success({
message: 'View Updated Successfully',
});
refetchAllView();
},
onError: (err) => {
showErrorNotification(err);
},
},
);
};
const moreOptionItems: DropdownItemType[] = [
{
type: 'item',
value: 'delete',
label: <Typography.Text strong>Delete</Typography.Text>,
onClick: onDeleteHandler,
prefix: <Trash2 size="md" />,
},
];
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
const saveButtonIcon = isQueryUpdated ? null : <Save size="md" />;
const showSaveView = false;
return (
<>
{showSaveView && (
<ExplorerCardHeadContainer size="small">
<Row align="middle">
<Col span={6}>
<Space>
<Typography>Query Builder</Typography>
<TextToolTip
url={ExploreHeaderToolTip.url}
text={ExploreHeaderToolTip.text}
useFilledIcon={false}
/>
</Space>
</Col>
<OffSetCol span={18}>
<Space size="large">
{viewsData?.data.data && viewsData?.data.data.length && (
<Space>
<Select
getPopupContainer={popupContainer}
loading={isLoading || isRefetching}
showSearch
placeholder="Select a view"
dropdownStyle={DropDownOverlay}
dropdownMatchSelectWidth={false}
optionLabelProp="value"
value={viewName || undefined}
>
{viewsData?.data.data.map((view) => (
<Select.Option key={view.id} value={view.name}>
<MenuItemGenerator
viewName={view.name}
viewKey={viewKey}
createdBy={view.createdBy}
uuid={view.id}
refetchAllView={refetchAllView}
viewData={viewsData.data.data}
sourcePage={sourcepage}
/>
</Select.Option>
))}
</Select>
</Space>
)}
{isQueryUpdated && (
<Button type="primary" icon={<Save />} onClick={onUpdateQueryHandler}>
Save changes
</Button>
)}
<Popover
getPopupContainer={popupContainer}
placement="bottomLeft"
trigger="click"
content={
<SaveViewWithName
sourcePage={sourcepage}
handlePopOverClose={handleOpenChange}
refetchAllView={refetchAllView}
/>
}
showArrow={false}
open={isOpen}
onOpenChange={handleOpenChange}
>
<Button
type={saveButtonType}
icon={saveButtonIcon}
data-testid="traces-save-view-action"
>
{isQueryUpdated
? SaveButtonText.SAVE_AS_NEW_VIEW
: SaveButtonText.SAVE_VIEW}
</Button>
</Popover>
<Share2 onClick={onCopyUrlHandler} size="md" />
{viewKey && (
<Dropdown
items={moreOptionItems}
nativeButton
align="end"
side="bottom"
>
<Button type="text" size="small" icon={<Ellipsis size="md" />} />
</Dropdown>
)}
</Space>
</OffSetCol>
</Row>
</ExplorerCardHeadContainer>
)}
<div>{children}</div>
</>
);
}
export default ExplorerCard;

View File

@@ -1,103 +0,0 @@
import { MouseEvent, useCallback } from 'react';
import { Trash2 } from '@signozhq/icons';
import { Col, Row, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { useNotifications } from 'hooks/useNotifications';
import { MenuItemContainer } from './styles';
import { MenuItemLabelGeneratorProps } from './types';
import {
deleteViewHandler,
getViewDetailsUsingViewKey,
trimViewName,
} from './utils';
function MenuItemGenerator({
viewName,
viewKey,
createdBy,
uuid,
viewData,
refetchAllView,
sourcePage,
}: MenuItemLabelGeneratorProps): JSX.Element {
const { panelType, redirectWithQueryBuilderData, updateAllQueriesOperators } =
useQueryBuilder();
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const { notifications } = useNotifications();
const { mutateAsync: deleteViewAsync } = useDeleteView(uuid);
const onDeleteHandler = (event: MouseEvent<SVGSVGElement>): void => {
event.stopPropagation();
deleteViewHandler({
deleteViewAsync,
notifications,
panelType,
redirectWithQueryBuilderData,
refetchAllView,
viewId: uuid,
viewKey,
updateAllQueriesOperators,
sourcePage,
});
};
const onMenuItemSelectHandler = useCallback(
({ key }: { key: string }): void => {
const currentViewDetails = getViewDetailsUsingViewKey(key, viewData);
if (!currentViewDetails) {
return;
}
const { query, name, id, panelType: currentPanelType } = currentViewDetails;
handleExplorerTabChange(currentPanelType, {
query,
viewName: name,
viewKey: id,
});
},
[viewData, handleExplorerTabChange],
);
const onLabelClickHandler = (): void => {
onMenuItemSelectHandler({
key: uuid,
});
};
const newViewName = trimViewName(viewName);
return (
<MenuItemContainer onClick={onLabelClickHandler}>
<Row justify="space-between">
<Col span={22}>
<Row>
<Tooltip title={viewName}>
<Typography.Text strong>{newViewName}</Typography.Text>
</Tooltip>
</Row>
<Row>
<Typography.Text color="muted">Created by {createdBy}</Typography.Text>
</Row>
</Col>
<Col span={2}>
<Typography.Link>
<Trash2
role="img"
aria-label="Delete view"
onClick={onDeleteHandler}
size="md"
/>
</Typography.Link>
</Col>
</Row>
</MenuItemContainer>
);
}
export default MenuItemGenerator;

View File

@@ -1,79 +0,0 @@
import { useTranslation } from 'react-i18next';
import { Input } from '@signozhq/ui/input';
import { Card, Form } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSaveView } from 'hooks/saveViews/useSaveView';
import { useNotifications } from 'hooks/useNotifications';
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
import { SaveButton } from './styles';
import { SaveViewFormProps, SaveViewWithNameProps } from './types';
import { saveViewHandler } from './utils';
function SaveViewWithName({
sourcePage,
handlePopOverClose,
refetchAllView,
}: SaveViewWithNameProps): JSX.Element {
const [form] = Form.useForm<SaveViewFormProps>();
const { t } = useTranslation(['explorer']);
const { currentQuery, panelType, redirectWithQueryBuilderData } =
useQueryBuilder();
const { notifications } = useNotifications();
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
const { isLoading, mutateAsync: saveViewAsync } = useSaveView({
viewName: form.getFieldValue('viewName'),
compositeQuery,
sourcePage,
extraData: '',
});
const onSaveHandler = (): void => {
saveViewHandler({
compositeQuery,
handlePopOverClose,
extraData: '',
notifications,
panelType: panelType || PANEL_TYPES.LIST,
redirectWithQueryBuilderData,
refetchAllView,
saveViewAsync,
sourcePage,
viewName: form.getFieldValue('viewName'),
form,
});
};
return (
<Card>
<Typography>{t('name_of_the_view')}</Typography>
<Form form={form} onFinish={onSaveHandler} requiredMark>
<Form.Item
name={['viewName']}
required
rules={[
{
required: true,
message: 'Please enter view name',
},
]}
>
<Input placeholder="Enter Name" />
</Form.Item>
<SaveButton
htmlType="submit"
type="primary"
loading={isLoading}
data-testid="save-view-name-action-button"
>
Save
</SaveButton>
</Form>
</Card>
);
}
export default SaveViewWithName;

View File

@@ -1,32 +0,0 @@
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { ViewProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
export const viewMockData: ViewProps[] = [
{
id: 'view1',
name: 'View 1',
createdBy: 'User 1',
category: 'category 1',
compositeQuery: {} as ICompositeMetricQuery,
createdAt: '2021-07-07T06:31:00.000Z',
updatedAt: '2021-07-07T06:33:00.000Z',
extraData: '',
sourcePage: DataSource.TRACES,
tags: [],
updatedBy: 'User 1',
},
{
id: 'view2',
name: 'View 2',
createdBy: 'User 2',
category: 'category 2',
compositeQuery: {} as ICompositeMetricQuery,
createdAt: '2021-07-07T06:30:00.000Z',
updatedAt: '2021-07-07T06:30:00.000Z',
extraData: '',
sourcePage: DataSource.TRACES,
tags: [],
updatedBy: 'User 2',
},
];

View File

@@ -1,9 +0,0 @@
export const ExploreHeaderToolTip = {
url: 'https://signoz.io/docs/querying/overview/?utm_source=product&utm_medium=new-query-builder',
text: 'More details on how to use query builder',
};
export const SaveButtonText = {
SAVE_AS_NEW_VIEW: 'Save as new view',
SAVE_VIEW: 'Save view',
};

View File

@@ -1,29 +0,0 @@
import { Button, Card, Col } from 'antd';
import styled, { CSSProperties } from 'styled-components';
export const ExplorerCardHeadContainer = styled(Card)`
margin: 1rem 0;
padding: 0;
`;
export const OffSetCol = styled(Col)`
text-align: right;
`;
export const SaveButton = styled(Button)`
&&& {
margin: 1rem 0;
width: 5rem;
}
`;
export const DropDownOverlay: CSSProperties = {
maxHeight: '20rem',
overflowY: 'auto',
width: '20rem',
padding: 0,
};
export const MenuItemContainer = styled(Card)`
padding: 0;
`;

View File

@@ -1,96 +0,0 @@
import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { DataSource } from 'types/common/queryBuilder';
import { viewMockData } from '../__mock__/viewData';
import ExplorerCard from '../ExplorerCard';
const historyReplace = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
}),
useHistory: (): any => ({
...jest.requireActual('react-router-dom').useHistory(),
replace: historyReplace,
}),
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): any => ({
safeNavigate: jest.fn(),
}),
}));
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
}));
jest.mock('hooks/saveViews/useGetAllViews', () => ({
useGetAllViews: jest.fn(() => ({
data: { data: { data: viewMockData } },
isLoading: false,
error: null,
isRefetching: false,
refetch: jest.fn(),
})),
}));
jest.mock('hooks/saveViews/useUpdateView', () => ({
useUpdateView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
jest.mock('hooks/saveViews/useDeleteView', () => ({
useDeleteView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
// Mock usePreferenceSync
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
formatting: {
maxLines: 1,
format: 'table',
fontSize: 'small',
version: 1,
},
},
loading: false,
error: null,
updateColumns: jest.fn(),
updateFormatting: jest.fn(),
}),
}));
describe('ExplorerCard', () => {
it('renders a card with a title and a description', () => {
render(
<MockQueryClientProvider>
<PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</PreferenceContextProvider>
</MockQueryClientProvider>,
);
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
});
it('renders a save view button', () => {
render(
<MockQueryClientProvider>
<PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</PreferenceContextProvider>
</MockQueryClientProvider>,
);
expect(screen.queryByText('Save view')).not.toBeInTheDocument();
});
});

View File

@@ -1,63 +0,0 @@
import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { DataSource } from 'types/common/queryBuilder';
import { viewMockData } from '../__mock__/viewData';
import MenuItemGenerator from '../MenuItemGenerator';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
jest.mock('antd', () => ({
...jest.requireActual('antd'),
useForm: jest.fn().mockReturnValue({
onFinish: jest.fn(),
}),
}));
describe('MenuItemGenerator', () => {
it('should render MenuItemGenerator component', () => {
const screen = render(
<MockQueryClientProvider>
<MenuItemGenerator
viewName={viewMockData[0].name}
viewKey={viewMockData[0].id}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].id}
refetchAllView={jest.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
/>
</MockQueryClientProvider>,
);
expect(screen.getByText(viewMockData[0].name)).toBeInTheDocument();
});
it('should call onMenuItemSelectHandler on click of MenuItemGenerator', () => {
render(
<MockQueryClientProvider>
<MenuItemGenerator
viewName={viewMockData[0].name}
viewKey={viewMockData[0].id}
createdBy={viewMockData[0].createdBy}
uuid={viewMockData[0].id}
refetchAllView={jest.fn()}
viewData={viewMockData}
sourcePage={DataSource.TRACES}
/>
</MockQueryClientProvider>,
);
const spanElement = screen.getByRole('img', {
name: /delete view/i,
});
expect(spanElement).toBeInTheDocument();
});
});

View File

@@ -1,63 +0,0 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { fireEvent, render } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { DataSource } from 'types/common/queryBuilder';
import SaveViewWithName from '../SaveViewWithName';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.APPLICATION}/`,
}),
}));
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
jest.mock('hooks/queryBuilder/useGetPanelTypesQueryParam', () => ({
useGetPanelTypesQueryParam: jest.fn(() => 'mockedPanelType'),
}));
jest.mock('hooks/saveViews/useSaveView', () => ({
useSaveView: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
describe('SaveViewWithName', () => {
it('should render SaveViewWithName component', () => {
const screen = render(
<QueryClientProvider client={queryClient}>
<SaveViewWithName
sourcePage={DataSource.TRACES}
handlePopOverClose={jest.fn()}
refetchAllView={jest.fn()}
/>
</QueryClientProvider>,
);
expect(screen.getByText('Save')).toBeInTheDocument();
});
it('should call saveViewAsync on click of Save button', () => {
const screen = render(
<QueryClientProvider client={queryClient}>
<SaveViewWithName
sourcePage={DataSource.TRACES}
handlePopOverClose={jest.fn()}
refetchAllView={jest.fn()}
/>
</QueryClientProvider>,
);
fireEvent.click(screen.getByText('Save'));
expect(screen.getByText('Save')).toBeInTheDocument();
});
});

View File

@@ -1,92 +0,0 @@
import { UseMutateAsyncFunction } from 'react-query';
import { FormInstance } from 'antd';
import type { NotificationInstance } from 'antd/es/notification/interface';
import { AxiosResponse } from 'axios';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
DeleteViewPayloadProps,
SaveViewPayloadProps,
SaveViewProps,
ViewProps,
} from 'types/api/saveViews/types';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
export interface ExplorerCardProps {
sourcepage: DataSource;
children: React.ReactNode;
}
export type GetViewDetailsUsingViewKey = (
viewKey: string,
data: ViewProps[] | undefined,
) =>
| {
query: Query;
name: string;
id: string;
panelType: PANEL_TYPES;
extraData?: string;
}
| undefined;
export interface IsQueryUpdatedInViewProps {
viewKey: string;
data: ViewProps[] | undefined;
stagedQuery: Query | null;
currentPanelType: PANEL_TYPES | null;
options: OptionsQuery;
}
export interface SaveViewWithNameProps {
sourcePage: ExplorerCardProps['sourcepage'];
handlePopOverClose: VoidFunction;
refetchAllView: VoidFunction;
}
export interface SaveViewFormProps {
viewName: string;
}
export interface MenuItemLabelGeneratorProps {
viewName: string;
viewKey: string;
createdBy: string;
uuid: string;
viewData: ViewProps[];
refetchAllView: VoidFunction;
sourcePage: ExplorerCardProps['sourcepage'];
}
export interface SaveViewHandlerProps {
viewName: string;
compositeQuery: ICompositeMetricQuery;
sourcePage: ExplorerCardProps['sourcepage'];
extraData: string;
panelType: PANEL_TYPES | null;
notifications: NotificationInstance;
refetchAllView: SaveViewWithNameProps['refetchAllView'];
saveViewAsync: UseMutateAsyncFunction<
AxiosResponse<SaveViewPayloadProps>,
Error,
SaveViewProps,
SaveViewPayloadProps
>;
handlePopOverClose: SaveViewWithNameProps['handlePopOverClose'];
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
form: FormInstance<SaveViewFormProps>;
}
export interface DeleteViewHandlerProps {
deleteViewAsync: UseMutateAsyncFunction<DeleteViewPayloadProps, Error, string>;
refetchAllView: MenuItemLabelGeneratorProps['refetchAllView'];
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
notifications: NotificationInstance;
panelType: PANEL_TYPES | null;
viewKey: string;
viewId: string;
updateAllQueriesOperators: QueryBuilderContextType['updateAllQueriesOperators'];
sourcePage: ExplorerCardProps['sourcepage'];
}

View File

@@ -1,204 +0,0 @@
import type { NotificationInstance } from 'antd/es/notification/interface';
import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import isEqual from 'lodash-es/isEqual';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
DeleteViewHandlerProps,
GetViewDetailsUsingViewKey,
IsQueryUpdatedInViewProps,
SaveViewHandlerProps,
} from './types';
export const showErrorNotification = (
notifications: NotificationInstance,
err: Error,
): void => {
notifications.error({
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
});
};
export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
viewKey,
data,
) => {
const selectedView = data?.find((view) => view.id === viewKey);
if (selectedView) {
const { compositeQuery, name, id, extraData } = selectedView;
const query = mapQueryDataFromApi(compositeQuery);
return { query, name, id, panelType: compositeQuery.panelType, extraData };
}
return undefined;
};
export const omitIdFromQuery = (query: Query | null): any => ({
...query,
builder: {
...query?.builder,
queryData: query?.builder.queryData.map((queryData) => {
const { id: _aggregateAttributeId, ...rest } =
queryData.aggregateAttribute || {};
const newAggregateAttribute = rest;
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
const { id: _groupByAttributeId, ...rest } = groupByAttribute;
return rest;
});
const newItems = queryData.filters?.items?.map((item) => {
const { id: _itemId, ...newItem } = item;
if (item.key) {
const { id: _keyId, ...rest } = item.key;
return {
...newItem,
key: rest,
};
}
return newItem;
});
return {
...queryData,
aggregateAttribute: newAggregateAttribute,
groupBy: newGroupByAttributes,
filters: {
...queryData.filters,
items: newItems,
},
limit: queryData.limit ? queryData.limit : 0,
offset: queryData.offset ? queryData.offset : 0,
pageSize: queryData.pageSize ? queryData.pageSize : 0,
};
}),
},
});
export const isQueryUpdatedInView = ({
viewKey,
data,
stagedQuery,
currentPanelType,
options,
}: IsQueryUpdatedInViewProps): boolean => {
const currentViewDetails = getViewDetailsUsingViewKey(viewKey, data);
if (!currentViewDetails) {
return false;
}
const { query, panelType, extraData } = currentViewDetails;
// Omitting id from aggregateAttribute and groupBy
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
if (
updatedCurrentQuery?.builder === undefined ||
updatedCurrentQuery.clickhouse_sql === undefined ||
updatedCurrentQuery.promql === undefined
) {
return false;
}
return (
panelType !== currentPanelType ||
!isEqual(query.builder, updatedCurrentQuery?.builder) ||
!isEqual(query.clickhouse_sql, updatedCurrentQuery?.clickhouse_sql) ||
!isEqual(query.promql, updatedCurrentQuery?.promql) ||
!isEqual(
options?.selectColumns,
extraData && JSON.parse(extraData)?.selectColumns,
) ||
(stagedQuery?.builder?.queryData?.[0]?.dataSource === DataSource.LOGS &&
(!isEqual(options?.format, extraData && JSON.parse(extraData)?.format) ||
!isEqual(options?.maxLines, extraData && JSON.parse(extraData)?.maxLines) ||
!isEqual(options?.fontSize, extraData && JSON.parse(extraData)?.fontSize)))
);
};
export const saveViewHandler = ({
saveViewAsync,
refetchAllView,
notifications,
handlePopOverClose,
viewName,
compositeQuery,
sourcePage,
extraData,
redirectWithQueryBuilderData,
panelType,
form,
}: SaveViewHandlerProps): void => {
saveViewAsync(
{
viewName,
compositeQuery,
sourcePage,
extraData,
},
{
onSuccess: (data) => {
refetchAllView();
redirectWithQueryBuilderData(mapQueryDataFromApi(compositeQuery), {
[QueryParams.panelTypes]: panelType,
[QueryParams.viewName]: viewName,
[QueryParams.viewKey]: data.data.data,
});
notifications.success({
message: 'View Saved Successfully',
});
},
onError: (err) => {
showErrorNotification(notifications, err);
},
onSettled: () => {
handlePopOverClose();
form.resetFields();
},
},
);
};
export const deleteViewHandler = ({
deleteViewAsync,
refetchAllView,
redirectWithQueryBuilderData,
notifications,
panelType,
viewKey,
viewId,
updateAllQueriesOperators,
sourcePage,
}: DeleteViewHandlerProps): void => {
deleteViewAsync(viewKey, {
onSuccess: () => {
if (viewId === viewKey) {
redirectWithQueryBuilderData(
updateAllQueriesOperators(
initialQueriesMap[sourcePage],
panelType || PANEL_TYPES.LIST,
sourcePage,
),
{
[QueryParams.viewName]: '',
[QueryParams.panelTypes]: panelType,
[QueryParams.viewKey]: '',
},
);
}
notifications.success({
message: 'View Deleted Successfully',
});
refetchAllView();
},
onError: (err) => {
showErrorNotification(notifications, err);
},
});
};
export const trimViewName = (viewName: string): string => {
if (viewName.length > 20) {
return `${viewName.substring(0, 20)}...`;
}
return viewName;
};

View File

@@ -1,5 +1,5 @@
import { DrawerProps } from 'antd';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';

View File

@@ -4,7 +4,7 @@ import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import {
getBodyDisplayString,
getSanitizedLogBody,

View File

@@ -1,6 +1,6 @@
import { MouseEvent } from 'react';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';

View File

@@ -130,7 +130,6 @@ describe('QueryBuilderV2 + QueryV2 - base render', () => {
updateAllQueriesOperators,
updateQueriesData,
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
} as unknown as QueryBuilderContextType;

View File

@@ -136,7 +136,6 @@ describe('MetricsSelect - signal source switching (standalone)', () => {
updateAllQueriesOperators: ((q: any) => q) as any,
updateQueriesData: ((q: any) => q) as any,
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
});
});
@@ -293,7 +292,6 @@ describe('DataSource change - Logs to Traces', () => {
updateAllQueriesOperators: ((q: any) => q) as any,
updateQueriesData: ((q: any) => q) as any,
initQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(() => false),
isDefaultQuery: jest.fn(() => false),
});
});

View File

@@ -5,7 +5,6 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import SavedViewsHeader from 'container/SavedViews/SavedViewsHeader';
import SavedViewsPanel from 'container/SavedViews/SavedViewsPanel';
import SavedViewsRestore from 'container/SavedViews/SavedViewsRestore';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import QuickFilters from '../QuickFilters';
import { SIDEBAR_TOOLTIP_DELAY_MS } from './constants';
@@ -50,10 +49,9 @@ function QuickFiltersLayout({
children,
}: QuickFiltersLayoutProps): JSX.Element {
const [isViewsListOpen, setIsViewsListOpen] = useState(false);
const isSavedViewEnabled = useSavedViewEnabled();
const hasQuickFilters = !!quickFilterProps;
const hasSavedViews = !!savedViewProps && isSavedViewEnabled;
const hasSavedViews = !!savedViewProps;
const showSidebar = showFilters && (hasQuickFilters || hasSavedViews);

View File

@@ -1,6 +1,5 @@
import { Button } from '@signozhq/ui/button';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { QuickFiltersSource } from '../../types';
@@ -20,10 +19,6 @@ jest.mock('../QuickFiltersLayout.module.scss', () => ({
},
}));
jest.mock('hooks/useSavedViewEnabled', () => ({
useSavedViewEnabled: jest.fn(() => true),
}));
jest.mock('container/SavedViews/SavedViewsRestore', () => ({
__esModule: true,
default: ({ source }: { source: string }): JSX.Element => (
@@ -71,10 +66,6 @@ const drawer = (): HTMLElement =>
screen.getByTestId('quick-filters-layout-drawer');
describe('QuickFiltersLayout', () => {
beforeEach(() => {
jest.mocked(useSavedViewEnabled).mockReturnValue(true);
});
it('renders QuickFilters with the given props inside the filters pane', () => {
render(
<QuickFiltersLayout showFilters quickFilterProps={quickFilterProps}>
@@ -149,35 +140,6 @@ describe('QuickFiltersLayout', () => {
});
describe('saved views', () => {
it('ignores savedViewProps while the flag is off', () => {
jest.mocked(useSavedViewEnabled).mockReturnValue(false);
const { unmount } = render(
<QuickFiltersLayout
showFilters
quickFilterProps={quickFilterProps}
savedViewProps={savedViewProps}
>
<div>content</div>
</QuickFiltersLayout>,
);
expect(screen.getByTestId('quick-filters')).toBeInTheDocument();
expect(screen.queryByTestId('saved-views-header')).not.toBeInTheDocument();
expect(screen.queryByTestId('saved-views-panel')).not.toBeInTheDocument();
// Saved views only, so nothing is left to show.
unmount();
render(
<QuickFiltersLayout showFilters savedViewProps={savedViewProps}>
<div>content</div>
</QuickFiltersLayout>,
);
expect(
screen.queryByTestId('quick-filters-layout-filters'),
).not.toBeInTheDocument();
expect(screen.queryByTestId('saved-views-restore')).not.toBeInTheDocument();
});
it('opens the panel from the header and slides the quick filters drawer, keeping the same QuickFilters node', async () => {
render(
<QuickFiltersLayout

View File

@@ -21,7 +21,6 @@ export enum LOCALSTORAGE {
CHAT_SUPPORT = 'CHAT_SUPPORT',
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
@@ -47,5 +46,4 @@ export enum LOCALSTORAGE {
DASHBOARDS_LIST_VIEWS = 'DASHBOARDS_LIST_VIEWS',
DASHBOARD_V2_PANEL_COLUMN_WIDTHS = 'DASHBOARD_V2_PANEL_COLUMN_WIDTHS',
LLM_ATTRIBUTE_MAPPING_TEST_SPAN = 'LLM_ATTRIBUTE_MAPPING_TEST_SPAN',
SAVED_VIEW_ENABLED = 'SAVED_VIEW_ENABLED',
}

View File

@@ -50,8 +50,6 @@ const ROUTES = {
ROLE_EDIT: '/settings/roles/:roleId/edit',
MEMBERS_SETTINGS: '/settings/members',
SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces/saved-views',
TRACES_FUNNELS: '/traces/funnels',
TRACES_FUNNELS_DETAIL: '/traces/funnels/:funnelId',
WORKSPACE_LOCKED: '/workspace-locked',
@@ -69,7 +67,6 @@ const ROUTES = {
MESSAGING_QUEUES_OVERVIEW: '/messaging-queues/overview',
METRICS_EXPLORER: '/metrics-explorer/summary',
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
METRICS_EXPLORER_VOLUME_CONTROL: '/metrics-explorer/volume-control',
API_MONITORING_BASE: '/api-monitoring',
API_MONITORING: '/api-monitoring/explorer',
@@ -77,7 +74,6 @@ const ROUTES = {
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
METER: '/meter',
METER_EXPLORER: '/meter/explorer',
METER_EXPLORER_VIEWS: '/meter/explorer/views',
HOME_PAGE: '/',
PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId',
SERVICE_ACCOUNTS_SETTINGS: '/settings/service-accounts',

View File

@@ -1,15 +0,0 @@
.trigger {
position: absolute;
bottom: 24px;
right: 24px;
z-index: 10;
transition: transform 0.15s;
&:hover {
transform: scale(1.08);
}
&:active {
transform: scale(0.96);
}
}

View File

@@ -1,61 +0,0 @@
import { useCallback } from 'react';
import { matchPath, useLocation } from 'react-router-dom';
import { Button } from '@signozhq/ui/button';
import { Tooltip } from '@signozhq/ui/tooltip';
import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes';
import Noz from 'components/Noz/Noz';
import { NOZ_TOOLTIP_TITLE } from 'components/Noz/Noz.constants';
import { AIAssistantEvents, AIAssistantOpenSource } from '../events';
import { normalizePage } from '../hooks/useAIAssistantAnalyticsContext';
import {
openAIAssistant,
useAIAssistantStore,
} from '../store/useAIAssistantStore';
import styles from './AIAssistantTrigger.module.scss';
/**
* Floating action button anchored to the bottom-right of the content area.
* Hidden when the panel is already open or when on the full-screen AI Assistant page.
*/
export default function AIAssistantTrigger(): JSX.Element | null {
const { pathname } = useLocation();
const isDrawerOpen = useAIAssistantStore((s) => s.isDrawerOpen);
const isModalOpen = useAIAssistantStore((s) => s.isModalOpen);
const isFullScreenPage = !!matchPath(pathname, {
path: ROUTES.AI_ASSISTANT,
exact: true,
});
const handleOpen = useCallback((): void => {
void logEvent(AIAssistantEvents.Opened, {
source: AIAssistantOpenSource.Icon,
currentPage: normalizePage(pathname),
});
openAIAssistant();
}, [pathname]);
if (isDrawerOpen || isModalOpen || isFullScreenPage) {
return null;
}
return (
<div className={`${styles.trigger} noz-wave`}>
<Tooltip title={NOZ_TOOLTIP_TITLE}>
<Button
size="md"
variant="solid"
color="primary"
icon
onClick={handleOpen}
aria-label="Open Noz"
>
<Noz size={24} />
</Button>
</Tooltip>
</div>
);
}

View File

@@ -1,2 +0,0 @@
export * from './AIAssistantTrigger';
export { default } from './AIAssistantTrigger';

View File

@@ -102,7 +102,7 @@ describe('resolvePageType', () => {
expect(resolvePageType(ROUTES.METRICS_EXPLORER_EXPLORER, '')).toBe(
PageTypeDTO.metrics_explorer,
);
expect(resolvePageType(ROUTES.METRICS_EXPLORER_VIEWS, '')).toBe(
expect(resolvePageType('/metrics-explorer/views', '')).toBe(
PageTypeDTO.metrics_explorer,
);
});

View File

@@ -235,7 +235,6 @@ describe('buildExplorerNavigationUrl', () => {
{ queryType: 'builder' } as never,
{
[QueryParams.panelTypes]: PANEL_TYPES.LIST,
[QueryParams.viewName]: 'My view',
[QueryParams.viewKey]: 'view-1',
},
);
@@ -259,7 +258,7 @@ describe('openSavedView', () => {
expect(pushedUrl).toContain(ROUTES.LOGS_EXPLORER);
const params = new URLSearchParams(pushedUrl.split('?')[1]);
expect(params.get(QueryParams.viewKey)).toBe('"view-logs"');
expect(params.get(QueryParams.viewName)).toBe('"View view-logs"');
expect(params.get(QueryParams.viewName)).toBeNull();
expect(params.get(QueryParams.panelTypes)).toBe('"list"');
});

View File

@@ -9,7 +9,7 @@ import { SavedViewSourcePage } from 'container/SavedViews/types';
import { findSavedView } from 'container/SavedViews/utils/findSavedView';
import { getSavedViewQuery } from 'container/SavedViews/utils/getSavedViewQuery';
import { toSavedViewSource } from 'container/SavedViews/utils/toSavedViewSource';
import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants';
import { SOURCEPAGE_VS_ROUTES } from 'container/SavedViews/constants';
import { DataSource } from 'types/common/queryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { History } from 'history';
@@ -108,7 +108,6 @@ export function openSavedView(
const query = getSavedViewQuery(view);
const url = buildExplorerNavigationUrl(route, query, {
[QueryParams.panelTypes]: view.spec.panelType as unknown as PANEL_TYPES,
[QueryParams.viewName]: view.spec.displayName,
[QueryParams.viewKey]: view.id,
});
history.push(url);

View File

@@ -13,7 +13,7 @@ import { matchPath } from 'react-router-dom';
*
* **URL-only.** All inputs come from `pathname` + `search`; we never reach
* into Redux / context. SigNoz already encodes most page state into the URL
* (`compositeQuery`, `startTime`/`endTime`, `viewName`/`viewKey`,
* (`compositeQuery`, `startTime`/`endTime`, `viewKey`,
* `variables`, `expandedWidgetId`, `activeLogId`, …), so URL parsing is
* sufficient. Anything not in the URL is simply omitted from `metadata` —
* the server applies its own defaults.
@@ -288,7 +288,7 @@ export function getAutoContexts(
// ── Metrics ───────────────────────────────────────────────────────────────
// Metrics explorer — `/metrics-explorer` and sub-routes (summary, explorer, views).
// Metrics explorer — `/metrics-explorer` and sub-routes (summary, explorer).
if (
matchPath(pathname, { path: ROUTES.METRICS_EXPLORER_BASE, exact: false })
) {
@@ -361,10 +361,6 @@ function collectSharedMetadata(
if (viewKey) {
out.savedViewId = viewKey;
}
const viewName = params.get(QueryParams.viewName);
if (viewName) {
out.savedViewName = viewName;
}
const variablesRaw = params.get(QueryParams.variables);
if (variablesRaw) {
try {

View File

@@ -72,40 +72,6 @@
}
}
.chat-support-gateway {
position: fixed;
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
// UI belongs in the bounded layout, not in another offset here.
bottom: calc(20px + var(--bottom-strip-height, 0px));
right: 20px;
z-index: 1000;
.chat-support-gateway-btn {
max-width: 48px;
width: 48px;
max-height: 48px;
height: 48px;
padding: 12px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
background-color: #f25733;
border: none;
&:hover {
color: white !important;
border-color: white !important;
}
.chat-support-gateway-btn-icon {
fill: white;
}
}
}
.add-credit-card-btn,
.cancel-btn {
display: inline-flex;

View File

@@ -28,7 +28,6 @@ import getUserLatestVersion from 'api/v1/version/getLatestVersion';
import { AxiosError } from 'axios';
import cx from 'classnames';
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import RefreshPaymentStatus from 'components/RefreshPaymentStatus/RefreshPaymentStatus';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
@@ -51,8 +50,6 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useNotifications } from 'hooks/useNotifications';
import { ChatSupportState, useChatSupport } from 'hooks/useChatSupport';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import useTabVisibility from 'hooks/useTabFocus';
import history from 'lib/history';
import { isNull } from 'lodash-es';
@@ -401,7 +398,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}, [pathname]);
const isToDisplayLayout = isLoggedIn;
const isSavedViewEnabled = useSavedViewEnabled();
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
const pageTitle = t(routeKey);
@@ -489,8 +485,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}
}, [isDarkMode]);
const chatSupport = useChatSupport();
// Listen for API warnings
const handleWarning = (
isSlow: boolean,
@@ -834,9 +828,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</LayoutContent>
</Sentry.ErrorBoundary>
{isSavedViewEnabled && isToDisplayLayout && !renderFullScreen && (
<BottomStrip />
)}
{isToDisplayLayout && !renderFullScreen && <BottomStrip />}
</div>
{isLoggedIn && isAIAssistantEnabled && (
@@ -847,9 +839,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)}
</Flex>
{chatSupport === ChatSupportState.NeedsCard && !isSavedViewEnabled && (
<ChatSupportGateway />
)}
{showChangelogModal && changelog && (
<ChangelogModal changelog={changelog} onClose={toggleChangelogModal} />
)}

View File

@@ -3,7 +3,6 @@ import { Color } from '@signozhq/design-tokens';
import { Spin } from 'antd';
import { TIMEZONE_DATA } from 'components/CustomTimePicker/timezoneUtils';
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
import { getRandomColor } from 'container/ExplorerOptions/utils';
import { AlertDetectionTypes } from 'container/FormAlertRules';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
@@ -54,7 +53,9 @@ export function getColorForThreshold(thresholdLabel: string): string {
if (thresholdLabel === 'info') {
return Color.BG_ROBIN_500;
}
return getRandomColor();
const colorKeys = Object.keys(Color) as (keyof typeof Color)[];
const randomKey = colorKeys[Math.floor(Math.random() * colorKeys.length)];
return Color[randomKey];
}
export function parseGoTime(input: string): {

View File

@@ -1,47 +0,0 @@
import { useEffect, useState } from 'react';
import ExplorerOptions, { ExplorerOptionsProps } from './ExplorerOptions';
import { getExplorerToolBarVisibility } from './utils';
type ExplorerOptionsWrapperProps = Omit<
ExplorerOptionsProps,
'isExplorerOptionDrop'
>;
function ExplorerOptionWrapper({
disabled,
query,
isLoading,
onExport,
sourcepage,
isOneChartPerQuery,
splitedQueries,
signalSource,
handleChangeSelectedView,
}: ExplorerOptionsWrapperProps): JSX.Element {
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
useEffect(() => {
const toolbarVisibility = getExplorerToolBarVisibility(sourcepage);
setIsExplorerOptionHidden(!toolbarVisibility);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<ExplorerOptions
disabled={disabled}
query={query}
isLoading={isLoading}
onExport={onExport}
sourcepage={sourcepage}
signalSource={signalSource}
isExplorerOptionHidden={isExplorerOptionHidden}
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
isOneChartPerQuery={isOneChartPerQuery}
splitedQueries={splitedQueries}
handleChangeSelectedView={handleChangeSelectedView}
/>
);
}
export default ExplorerOptionWrapper;

View File

@@ -1,234 +0,0 @@
.explorer-options-container {
position: fixed;
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
// UI belongs in the bounded layout, not in another offset here.
bottom: var(--bottom-strip-height, 0px);
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
display: flex;
gap: 8px;
background-color: transparent;
.multi-alert-button,
.multi-dashboard-button {
min-width: 130px;
.ant-select-selector {
.ant-select-selection-placeholder {
margin-left: 0 !important;
}
.ant-select-arrow {
display: none !important;
}
}
}
&__divider {
--divider-vertical-margin: 0;
}
}
.hide-update {
left: calc(50% - 72px) !important;
}
.explorer-update {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: color-mix(in srgb, var(--l1-border) 60%, transparent);
border: 1px solid var(--l1-border);
border-radius: 4px;
backdrop-filter: blur(20px);
box-sizing: border-box;
.action-icon {
display: flex;
justify-content: center;
align-items: center;
padding: 8px;
border-radius: 50px;
border: 1px solid var(--l1-border);
background: var(--l1-border);
cursor: pointer;
box-shadow: none;
}
.hidden {
display: none;
}
}
.explorer-options {
padding: 10px 12px;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
backdrop-filter: blur(20px);
cursor: default;
display: flex;
gap: 16px;
z-index: 1;
.ant-select-selector {
padding: 0 !important;
}
hr {
border-color: var(--l1-border);
}
.view-options,
.actions {
.info-icon {
margin: 8px;
}
.hidden {
display: none;
}
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
.ant-select-focused {
border-color: transparent !important;
.ant-select-selector {
border-color: transparent !important;
box-shadow: none !important;
}
}
.ant-select-selector {
border: transparent !important;
background-color: transparent !important;
.ant-select-selection-placeholder {
margin-left: 12px;
}
}
}
.hidden {
display: none;
}
button {
display: flex;
align-items: center;
justify-content: center;
}
}
.app-content {
&.collapsed {
.explorer-options-container {
left: calc(50% + 72px);
}
}
}
.render-options {
display: flex;
align-items: center;
gap: 8px;
padding: 0 2px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.dot {
margin-left: 6px;
min-height: 6px;
min-width: 6px;
border-radius: 50%;
backdrop-filter: blur(20px);
}
}
.save-view-modal {
width: 384px !important;
.ant-modal-content {
padding: 0;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
padding: 16px;
background: var(--l2-background);
border-bottom: 1px solid var(--l1-border);
}
.ant-modal-body {
padding: 12px 16px 0px 16px;
.ant-typography {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
}
.save-view-input {
margin-top: 8px;
display: flex;
gap: 8px;
}
.ant-color-picker-trigger {
padding: 6px;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
width: 32px;
height: 32px;
.ant-color-picker-color-block {
border-radius: 50px;
width: 16px;
height: 16px;
flex-shrink: 0;
.ant-color-picker-color-block-inner {
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
.ant-modal-footer {
display: flex;
justify-content: flex-end;
padding: 16px 16px;
margin: 0;
.save-button {
display: flex;
}
}
}
.title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +0,0 @@
.explorer-option-droppable-container {
position: fixed;
// Lifted above the bottom strip. Don't extend this pattern — new fixed-bottom
// UI belongs in the bounded layout, not in another offset here.
bottom: var(--bottom-strip-height, 0px);
width: -webkit-fill-available;
height: 24px;
display: flex;
justify-content: center;
border-radius: 10px 10px 0px 0px;
// box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
// backdrop-filter: blur(20px);
.explorer-actions-btn {
display: flex;
gap: 8px;
margin-right: 8px;
.action-btn {
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px 10px 0px 0px;
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
height: 24px !important;
border: none;
}
}
.explorer-show-btn {
border-radius: 6px 6px 0px 0px;
border: 1px solid var(--l1-border);
background: color-mix(in srgb, var(--l1-border) 40%, transparent);
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(20px);
align-self: center;
padding: 8px 12px;
height: 24px !important;
.menu-bar {
border-radius: 50px;
background: var(--l3-background);
height: 4px;
width: 50px;
}
}
}

View File

@@ -1,84 +0,0 @@
import { Dispatch, SetStateAction } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Button, Tooltip } from 'antd';
import { Disc3, X } from '@signozhq/icons';
import { DataSource } from 'types/common/queryBuilder';
import { setExplorerToolBarVisibility } from './utils';
import './ExplorerOptionsHideArea.styles.scss';
interface DroppableAreaProps {
viewName: string;
isQueryUpdated: boolean;
isExplorerOptionHidden?: boolean;
sourcepage: DataSource;
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
handleClearSelect: () => void;
onUpdateQueryHandler: () => void;
isEditDeleteSupported: boolean;
}
function ExplorerOptionsHideArea({
viewName,
isQueryUpdated,
isExplorerOptionHidden,
sourcepage,
setIsExplorerOptionHidden,
handleClearSelect,
onUpdateQueryHandler,
isEditDeleteSupported,
}: DroppableAreaProps): JSX.Element {
const handleShowExplorerOption = (): void => {
if (setIsExplorerOptionHidden) {
setIsExplorerOptionHidden(false);
setExplorerToolBarVisibility(true, sourcepage);
}
};
return (
<div className="explorer-option-droppable-container">
{isExplorerOptionHidden && (
<>
{viewName && (
<div className="explorer-actions-btn">
<Tooltip title="Clear this view">
<Button
onClick={handleClearSelect}
className="action-btn"
style={{ background: Color.BG_CHERRY_500 }}
icon={<X size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
{isEditDeleteSupported && isQueryUpdated && (
<Tooltip title="Update this View">
<Button
onClick={onUpdateQueryHandler}
className="action-btn"
style={{ background: Color.BG_ROBIN_500 }}
icon={<Disc3 size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
)}
</div>
)}
<Button
// style={{ alignSelf: 'center', marginRight: 'calc(10% - 20px)' }}
className="explorer-show-btn"
onClick={handleShowExplorerOption}
data-testid="show-explorer-option"
>
<div className="menu-bar" />
</Button>
</>
)}
</div>
);
}
ExplorerOptionsHideArea.defaultProps = {
isExplorerOptionHidden: undefined,
setIsExplorerOptionHidden: undefined,
};
export default ExplorerOptionsHideArea;

View File

@@ -1,391 +0,0 @@
import { useHistory } from 'react-router-dom';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { MOCK_QUERY } from 'container/QueryTable/Drilldown/__tests__/mockTableData';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { rest, server } from 'mocks-server/server';
import {
defaultFeatureFlags,
render,
screen,
userEvent,
waitFor,
} from 'tests/test-utils';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { buildExportPanelLink } from 'pages/DashboardPage/DashboardContainer/PanelEditor/newPanelRoute';
import ExplorerOptionWrapper from '../ExplorerOptionWrapper';
import { getExplorerToolBarVisibility } from '../utils';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: jest.fn(),
}));
jest.mock('../utils', () => ({
getExplorerToolBarVisibility: jest.fn(),
generateRGBAFromHex: jest.fn(() => 'rgba(0, 0, 0, 0.08)'),
getRandomColor: jest.fn(() => '#000000'),
saveNewViewHandler: jest.fn(),
setExplorerToolBarVisibility: jest.fn(),
DATASOURCE_VS_ROUTES: {},
}));
const mockGetExplorerToolBarVisibility = jest.mocked(
getExplorerToolBarVisibility,
);
const mockUseHistory = jest.mocked(useHistory);
// Mock data
const TEST_QUERY_ID = 'test-query-id';
const TEST_DASHBOARD_ID = 'test-dashboard-id';
const TEST_DASHBOARD_TITLE_2 = 'Test Dashboard for Export';
const NEW_DASHBOARD_ID = 'new-dashboard-id';
// The export dialog talks to the generated Perses-spec dashboard endpoints.
const V2_LIST_ENDPOINT = '*/api/v2/users/me/dashboards';
const V2_CREATE_ENDPOINT = '*/api/v2/dashboards';
// "Create new dashboard" names the dashboard from the `new_dashboard_title` i18n
// key; the test-utils react-i18next mock returns the key verbatim.
const NEW_DASHBOARD_TITLE = 'new_dashboard_title';
// Use the existing mock query from the codebase
const mockQuery: Query = {
...MOCK_QUERY,
id: TEST_QUERY_ID, // Override with our test ID
} as Query;
const ADD_TO_DASHBOARD_BUTTON_NAME = /add to dashboard/i;
interface PickerDashboard {
id: string;
title: string;
}
/** Register the "list dashboards" endpoint the picker reads. */
function mockDashboardList(dashboards: PickerDashboard[]): void {
server.use(
rest.get(V2_LIST_ENDPOINT, (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
dashboards: dashboards.map(({ id, title }) => ({
id,
name: title,
spec: { display: { name: title } },
})),
reservedKeywords: [],
},
}),
),
),
);
}
/** Register the "create dashboard" endpoint the "New dashboard" button hits. */
function mockCreateDashboard(id: string): void {
server.use(
rest.post(V2_CREATE_ENDPOINT, (_req, res, ctx) =>
res(ctx.status(201), ctx.json({ status: 'success', data: { id } })),
),
);
}
// Helper function to render component with props
const renderExplorerOptionWrapper = (
overrides = {},
): ReturnType<typeof render> => {
const props = {
disabled: false,
query: mockQuery,
isLoading: false,
onExport: jest.fn() as jest.MockedFunction<
(
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
>,
sourcepage: DataSource.LOGS,
isOneChartPerQuery: false,
splitedQueries: [],
signalSource: 'test-signal',
...overrides,
};
return render(
<ExplorerOptionWrapper
disabled={props.disabled}
query={props.query}
isLoading={props.isLoading}
onExport={props.onExport}
sourcepage={props.sourcepage}
isOneChartPerQuery={props.isOneChartPerQuery}
splitedQueries={props.splitedQueries}
signalSource={props.signalSource}
/>,
undefined,
{ appContextOverrides: { featureFlags: defaultFeatureFlags } },
);
};
describe('ExplorerOptionWrapper', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetExplorerToolBarVisibility.mockReturnValue(true);
});
it('should navigate to alert creation page when "Create an Alert" is clicked in logs-explorer', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockPush = jest.fn();
mockUseHistory.mockReturnValue({
push: mockPush,
} as unknown as ReturnType<typeof useHistory>);
renderExplorerOptionWrapper({ sourcepage: DataSource.LOGS });
const createAlertButton = screen.getByRole('button', {
name: 'Create an Alert',
});
await user.click(createAlertButton);
expect(mockPush).toHaveBeenCalledTimes(1);
const calledWith = mockPush.mock.calls[0][0] as string;
const [path, search = ''] = calledWith.split('?');
expect(path).toBe('/alerts/new');
const params = new URLSearchParams(search);
expect(params.has('compositeQuery')).toBe(true);
});
describe('onExport functionality', () => {
it('should call onExport when New Dashboard button is clicked in export modal', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const testOnExport = jest.fn() as jest.MockedFunction<
(
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
>;
mockCreateDashboard(NEW_DASHBOARD_ID);
renderExplorerOptionWrapper({ onExport: testOnExport });
// Find and click the "Add to Dashboard" button
const addToDashboardButton = screen.getByRole('button', {
name: ADD_TO_DASHBOARD_BUTTON_NAME,
});
await user.click(addToDashboardButton);
// Wait for the export modal to appear
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
// Click the "New dashboard" button
const newDashboardButton = screen.getByTestId('export-panel-new-dashboard');
await user.click(newDashboardButton);
// Wait for the API call to complete and onExport to be called
await waitFor(() => {
expect(testOnExport).toHaveBeenCalledWith(
{ id: NEW_DASHBOARD_ID, title: NEW_DASHBOARD_TITLE },
true,
);
});
});
it('should call onExport when selecting existing dashboard and clicking Export button', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const testOnExport = jest.fn() as jest.MockedFunction<
(
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
>;
// Mock existing dashboards with unique titles
const dashboards: PickerDashboard[] = [
{ id: 'dashboard-1', title: 'Dashboard 1' },
{ id: 'dashboard-2', title: 'Dashboard 2' },
];
mockDashboardList(dashboards);
renderExplorerOptionWrapper({ onExport: testOnExport });
// Find and click the "Add to Dashboard" button
const addToDashboardButton = screen.getByRole('button', {
name: ADD_TO_DASHBOARD_BUTTON_NAME,
});
await user.click(addToDashboardButton);
// Wait for the export modal to appear
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
// Wait for the dashboard select to render AND finish loading. antd
// disables the Select while the list request is in flight, and a click
// on a disabled combobox is a no-op — so sync on the ready (enabled)
// state before clicking.
const modal = screen.getByRole('dialog');
await waitFor(() => {
const combobox = modal.querySelector('[role="combobox"]');
expect(combobox).toBeTruthy();
expect(combobox).not.toBeDisabled();
});
const dashboardSelect = modal.querySelector(
'[role="combobox"]',
) as HTMLElement;
expect(dashboardSelect).toBeInTheDocument();
await user.click(dashboardSelect);
// Wait for the dropdown options to appear and select the first dashboard
await waitFor(() => {
expect(screen.getByText(dashboards[0].title)).toBeInTheDocument();
});
// Click on the first dashboard option
const dashboardOption = screen.getByText(dashboards[0].title);
await user.click(dashboardOption);
// Wait for the selection to be made and the export button to be enabled
await waitFor(() => {
expect(screen.getByTestId('export-panel-export')).not.toHaveAttribute(
'aria-disabled',
'true',
);
});
// Click the export button
const exportButton = screen.getByTestId('export-panel-export');
await user.click(exportButton);
// Wait for onExport to be called with the selected dashboard
await waitFor(() => {
expect(testOnExport).toHaveBeenCalledWith(
{ id: dashboards[0].id, title: dashboards[0].title },
false,
);
});
});
it('should navigate to the panel editor via the export link without writing the dashboard', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
// Mock the safeNavigate function
const mockSafeNavigate = jest.fn();
const panelTypeParam = PANEL_TYPES.TIME_SERIES;
const query = mockQuery;
// Create a real handleExport function similar to LogsExplorerViews
// Export navigates only; it must not write the dashboard (PR #8029).
const handleExport = (dashboard: ExportDashboard | null): void => {
if (!dashboard) {
return;
}
// Call the real link builder (not mocked)
const dashboardEditView = buildExportPanelLink({
query,
panelType: panelTypeParam,
dashboardId: dashboard.id,
});
// Simulate navigation
mockSafeNavigate(dashboardEditView);
};
// Mock existing dashboards
const dashboards: PickerDashboard[] = [
{ id: TEST_DASHBOARD_ID, title: TEST_DASHBOARD_TITLE_2 },
];
mockDashboardList(dashboards);
renderExplorerOptionWrapper({ onExport: handleExport });
// Find and click the "Add to Dashboard" button
const addToDashboardButton = screen.getByRole('button', {
name: ADD_TO_DASHBOARD_BUTTON_NAME,
});
await user.click(addToDashboardButton);
// Wait for the export modal to appear
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
// Wait for the dashboard select to render AND finish loading (see note above).
const modal = screen.getByRole('dialog');
await waitFor(() => {
const combobox = modal.querySelector('[role="combobox"]');
expect(combobox).toBeTruthy();
expect(combobox).not.toBeDisabled();
});
const dashboardSelect = modal.querySelector(
'[role="combobox"]',
) as HTMLElement;
expect(dashboardSelect).toBeInTheDocument();
await user.click(dashboardSelect);
// Wait for the dropdown options to appear and select the dashboard
await waitFor(() => {
expect(screen.getByText(dashboards[0].title)).toBeInTheDocument();
});
// Click on the dashboard option
const dashboardOption = screen.getByText(dashboards[0].title);
await user.click(dashboardOption);
// Wait for the selection to be made and the export button to be enabled
await waitFor(() => {
expect(screen.getByTestId('export-panel-export')).not.toHaveAttribute(
'aria-disabled',
'true',
);
});
// Click the export button
const exportButton = screen.getByTestId('export-panel-export');
await user.click(exportButton);
// Wait for the handleExport function to be called and navigation to occur
await waitFor(() => {
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
// V2 panel-editor link: compositeQuery is double-encoded (see newPanelRoute).
expect(mockSafeNavigate).toHaveBeenCalledWith(
`/dashboard/${TEST_DASHBOARD_ID}/panel/new?panelKind=signoz%2FTimeSeriesPanel&compositeQuery=${encodeURIComponent(
encodeURIComponent(JSON.stringify(query)),
)}`,
);
});
});
});
it('should not show export buttons when component is disabled', () => {
const testOnExport = jest.fn() as jest.MockedFunction<
(
dashboard: ExportDashboard | null,
isNewDashboard?: boolean,
queryToExport?: Query,
) => void
>;
renderExplorerOptionWrapper({ disabled: true, onExport: testOnExport });
// The "Add to Dashboard" button should be disabled
const addToDashboardButton = screen.getByRole('button', {
name: ADD_TO_DASHBOARD_BUTTON_NAME,
});
expect(addToDashboardButton).toBeDisabled();
});
});

View File

@@ -1,6 +0,0 @@
export enum PreservedViewsTypes {
LOGS = 'logs',
TRACES = 'traces',
METRICS = 'metrics',
METER = 'meter',
}

View File

@@ -1,47 +0,0 @@
import { Dispatch, SetStateAction } from 'react';
import { UseMutateAsyncFunction } from 'react-query';
import type { NotificationInstance } from 'antd/es/notification/interface';
import { AxiosResponse } from 'axios';
import { SaveViewWithNameProps } from 'components/ExplorerCard/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
import { PreservedViewsTypes } from './constants';
export interface SaveNewViewHandlerProps {
viewName: string;
compositeQuery: ICompositeMetricQuery;
sourcePage: DataSource | 'meter';
extraData: SaveViewProps['extraData'];
panelType: PANEL_TYPES | null;
notifications: NotificationInstance;
refetchAllView: SaveViewWithNameProps['refetchAllView'];
saveViewAsync: UseMutateAsyncFunction<
AxiosResponse<SaveViewPayloadProps>,
Error,
SaveViewProps,
SaveViewPayloadProps
>;
handlePopOverClose: SaveViewWithNameProps['handlePopOverClose'];
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
setNewViewName: Dispatch<SetStateAction<string>>;
}
export type PreservedViewType =
| PreservedViewsTypes.LOGS
| PreservedViewsTypes.TRACES
| PreservedViewsTypes.METRICS
| PreservedViewsTypes.METER;
export type PreservedViewsInLocalStorage = Partial<
Record<PreservedViewType, { key: string; value: string }>
>;
export type ChangeViewFunctionType = (
view: ExplorerViews,
querySearchParameters?: ICurrentQueryData,
) => void;

View File

@@ -1,123 +0,0 @@
import { Color } from '@signozhq/design-tokens';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { showErrorNotification } from 'components/ExplorerCard/utils';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { DataSource } from 'types/common/queryBuilder';
import { SaveNewViewHandlerProps } from './types';
export const getRandomColor = (): string => {
const colorKeys = Object.keys(Color) as (keyof typeof Color)[];
const randomKey = colorKeys[Math.floor(Math.random() * colorKeys.length)];
return Color[randomKey];
};
export const DATASOURCE_VS_ROUTES: Record<DataSource, string> = {
[DataSource.METRICS]: ROUTES.METRICS_EXPLORER_EXPLORER,
[DataSource.TRACES]: ROUTES.TRACES_EXPLORER,
[DataSource.LOGS]: ROUTES.LOGS_EXPLORER,
};
export const saveNewViewHandler = ({
saveViewAsync,
refetchAllView,
notifications,
handlePopOverClose,
viewName,
compositeQuery,
sourcePage,
extraData,
redirectWithQueryBuilderData,
panelType,
setNewViewName,
}: SaveNewViewHandlerProps): void => {
saveViewAsync(
{
viewName,
compositeQuery,
sourcePage: sourcePage as DataSource,
extraData,
},
{
onSuccess: (data) => {
refetchAllView();
redirectWithQueryBuilderData(mapQueryDataFromApi(compositeQuery), {
[QueryParams.panelTypes]: panelType,
[QueryParams.viewName]: viewName,
[QueryParams.viewKey]: data.data.data,
});
notifications.success({
message: 'View Saved Successfully',
});
},
onError: (err) => {
showErrorNotification(notifications, err);
},
onSettled: () => {
handlePopOverClose();
setNewViewName('');
},
},
);
};
export const generateRGBAFromHex = (hex: string, opacity: number): string =>
`rgba(${parseInt(hex.slice(1, 3), 16)}, ${parseInt(
hex.slice(3, 5),
16,
)}, ${parseInt(hex.slice(5, 7), 16)}, ${opacity})`;
export const getExplorerToolBarVisibility = (dataSource: string): boolean => {
try {
const showExplorerToolbar = getLocalStorageKey(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar === null) {
const parsedShowExplorerToolbar: {
[DataSource.LOGS]: boolean;
[DataSource.TRACES]: boolean;
[DataSource.METRICS]: boolean;
} = {
[DataSource.METRICS]: true,
[DataSource.TRACES]: true,
[DataSource.LOGS]: true,
};
setLocalStorageKey(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);
return true;
}
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar || '{}');
return parsedShowExplorerToolbar[dataSource];
} catch (error) {
console.error(error);
return false;
}
};
export const setExplorerToolBarVisibility = (
value: boolean,
dataSource: string,
): void => {
try {
const showExplorerToolbar = getLocalStorageKey(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
);
if (showExplorerToolbar) {
const parsedShowExplorerToolbar = JSON.parse(showExplorerToolbar);
parsedShowExplorerToolbar[dataSource] = value;
setLocalStorageKey(
LOCALSTORAGE.SHOW_EXPLORER_TOOLBAR,
JSON.stringify(parsedShowExplorerToolbar),
);
return;
}
} catch (error) {
console.error(error);
}
};

View File

@@ -10,9 +10,8 @@ import {
import ROUTES from 'constants/routes';
import { getSavedViewQuery } from 'container/SavedViews/utils/getSavedViewQuery';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import { SOURCEPAGE_VS_ROUTES } from 'pages/SaveView/constants';
import { SOURCEPAGE_VS_ROUTES } from 'container/SavedViews/constants';
import Card from 'periscope/components/Card/Card';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
@@ -36,7 +35,6 @@ export default function SavedViews({
loadingUserPreferences: boolean;
}): JSX.Element {
const { user } = useAppContext();
const isSavedViewEnabled = useSavedViewEnabled();
const [selectedEntity, setSelectedEntity] = useState<string>('logs');
const [selectedEntityViews, setSelectedEntityViews] = useState<
SavedviewtypesSavedViewDTO[]
@@ -104,7 +102,6 @@ export default function SavedViews({
view.spec.panelType,
{
query: getSavedViewQuery(view),
viewName: view.spec.displayName,
viewKey: view.id,
},
SOURCEPAGE_VS_ROUTES[selectedEntity],
@@ -117,19 +114,6 @@ export default function SavedViews({
}
}, [hasSavedViews, onUpdateChecklistDoneItem, loadingUserPreferences]);
const footerLink = useMemo(() => {
if (selectedEntity === 'logs') {
return ROUTES.LOGS_SAVE_VIEWS;
}
if (selectedEntity === 'traces') {
return ROUTES.TRACES_SAVE_VIEWS;
}
if (selectedEntity === 'metrics') {
return ROUTES.METRICS_EXPLORER_VIEWS;
}
return '';
}, [selectedEntity]);
const getStartedLink = useMemo(() => {
if (selectedEntity === 'logs') {
return ROUTES.LOGS_EXPLORER;
@@ -352,26 +336,6 @@ export default function SavedViews({
<Card.Content>
{selectedEntityViews.length > 0 ? renderSavedViews() : emptyStateCard()}
</Card.Content>
{!isSavedViewEnabled && selectedEntityViews.length > 0 && (
<Card.Footer>
<div className="services-footer home-data-card-footer">
<Link to={footerLink}>
<Button
type="link"
className="periscope-btn link learn-more-link"
onClick={(): void => {
logEvent('Homepage: All saved views clicked', {
entity: selectedEntity,
});
}}
>
All Views <ArrowRight size={12} />
</Button>
</Link>
</div>
</Card.Footer>
)}
</Card>
);
}

View File

@@ -23,7 +23,6 @@
.traces-explorer-views {
padding: 8px;
padding-bottom: 60px;
margin-bottom: 24px;
.ant-tabs-tabpane {

View File

@@ -3,7 +3,6 @@ import { useQueryClient } from 'react-query';
import { useSearchParams } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
@@ -225,11 +224,9 @@ function Explorer(): JSX.Element {
}
/>
</div>
<ExplorerCard sourcepage={DataSource.TRACES}>
<div className="query-section-container">
<QuerySection />
</div>
</ExplorerCard>
<div className="query-section-container">
<QuerySection />
</div>
<div className="traces-explorer-views">
{isCancelled && (

View File

@@ -6,7 +6,7 @@ import FieldsSelector from 'components/FieldsSelector';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import GoToTop from 'container/GoToTop';
import { useOptionsMenu } from 'container/OptionsMenu';
import { LOGS_REQUIRED_COLUMNS } from 'container/OptionsMenu/constants';

View File

@@ -1,4 +1,4 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import { ILog } from 'types/api/logs/log';
export interface ILiveLogsLog {

View File

@@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import { DataViewer } from 'periscope/components/DataViewer';
import { ILog } from 'types/api/logs/log';

View File

@@ -2,9 +2,11 @@ import { useCallback, useMemo } from 'react';
import { CircleMinus, CirclePlus, Layers, RefreshCw } from '@signozhq/icons';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import { FeatureKeys } from 'constants/features';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
import {
ChangeViewFunctionType,
ICurrentQueryData,
} from 'hooks/useHandleExplorerTabChange';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import {
FieldContext,

View File

@@ -1,4 +1,4 @@
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import APIError from 'types/api/error';
import { ILog } from 'types/api/logs/log';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';

View File

@@ -16,7 +16,6 @@ import setToLocalstorage from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { LOCALSTORAGE } from 'constants/localStorage';
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
import { QueryParams } from 'constants/query';
import {
initialFilters,
@@ -24,8 +23,7 @@ import {
PANEL_TYPES,
} from 'constants/queryBuilder';
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { ChangeViewFunctionType } from 'hooks/useHandleExplorerTabChange';
import GoToTop from 'container/GoToTop';
import LogsExplorerChart from 'container/LogsExplorerChart';
import LogsExplorerList from 'container/LogsExplorerList';
@@ -39,13 +37,9 @@ import {
} from 'container/LogsExplorerViews/explorerUtils';
import ExplorerActions from 'container/ExplorerActions/ExplorerActions';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { useGetExportToDashboardLink } from 'hooks/dashboard/useGetExportToDashboardLink';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import useUrlQueryData from 'hooks/useUrlQueryData';
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
import { isEmpty, isUndefined } from 'lodash-es';
@@ -59,7 +53,6 @@ import { Filter } from 'types/api/v5/queryRange';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 } from 'uuid';
import { getListOrderBy, setListOrderBy } from 'utils/explorerUtils';
import LogsActionsContainer from './LogsActionsContainer';
@@ -82,10 +75,6 @@ function LogsExplorerViewsContainer({
showLiveLogs: boolean;
handleChangeSelectedView: ChangeViewFunctionType;
}): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const isSavedViewEnabled = useSavedViewEnabled();
const getExportToDashboardLink = useGetExportToDashboardLink();
const [showFrequencyChart, setShowFrequencyChart] = useState(
() => getFromLocalstorage(LOCALSTORAGE.SHOW_FREQUENCY_CHART) === 'true',
);
@@ -276,49 +265,6 @@ function LogsExplorerViewsContainer({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.payload]);
const handleExport = useCallback(
(dashboard: ExportDashboard | null, isNewDashboard?: boolean): void => {
if (!dashboard || !selectedPanelType) {
return;
}
const panelTypeParam = AVAILABLE_EXPORT_PANEL_TYPES.includes(
selectedPanelType,
)
? selectedPanelType
: PANEL_TYPES.TIME_SERIES;
const widgetId = v4();
if (!exportDefaultQuery) {
return;
}
logEvent('Logs Explorer: Add to dashboard successful', {
panelType: selectedPanelType,
isNewDashboard,
dashboardName: dashboard?.title,
});
const dashboardEditView = getExportToDashboardLink({
query: exportDefaultQuery,
panelType: panelTypeParam,
dashboardId: dashboard.id,
widgetId,
});
if (dashboardEditView) {
safeNavigate(dashboardEditView);
}
},
[
safeNavigate,
exportDefaultQuery,
selectedPanelType,
getExportToDashboardLink,
],
);
useEffect(() => {
const currentData = data?.payload?.data?.newResult?.data?.result || [];
if (currentData.length > 0 && currentData[0].list) {
@@ -517,16 +463,6 @@ function LogsExplorerViewsContainer({
</div>
<GoToTop />
{!isSavedViewEnabled && (
<ExplorerOptionWrapper
disabled={!stagedQuery}
query={exportDefaultQuery}
onExport={handleExport}
sourcepage={DataSource.LOGS}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
</div>
);
}

View File

@@ -138,13 +138,6 @@ jest.mock(
return <div>MockQueryBuilderSearchV2</div>;
},
);
jest.mock(
'container/ExplorerOptions/ExplorerOptionWrapper',
() =>
function MockExplorerOptionWrapper(): JSX.Element {
return <div>MockExplorerOptionWrapper</div>;
},
);
jest.mock(
'components/QuickFilters/QuickFilters',
() =>
@@ -416,7 +409,6 @@ function LogsExplorerWithMockContext({
updateQueriesData: (): Query => initialQueriesMap.logs,
initQueryBuilderData: noop,
handleOnUnitsChange: noop,
isStagedQueryUpdated: (): boolean => false,
}),
[currentQuery, stagedQuery, handleRunQuery],
);

View File

@@ -200,5 +200,4 @@ export const mockQueryBuilderContextValue = {
updateQueriesData: (): Query => initialQueriesMap.logs,
initQueryBuilderData: noop,
handleOnUnitsChange: noop,
isStagedQueryUpdated: (): boolean => false,
};

View File

@@ -1,8 +1,5 @@
.meter-explorer-container {
.meter-explorer-content-section {
// Clearance for the fixed ExplorerOptions bar.
padding-bottom: 80px;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);

View File

@@ -10,38 +10,22 @@ import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalField
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { useGetExportToDashboardLink } from 'hooks/dashboard/useGetExportToDashboardLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import { Filter } from '@signozhq/icons';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events';
import TimeSeries from './TimeSeries';
import { splitQueryIntoOneChartPerQuery } from './utils';
import './Explorer.styles.scss';
function Explorer(): JSX.Element {
const quickFilterFieldApis = useSignalFieldApis();
const {
handleRunQuery,
stagedQuery,
updateAllQueriesOperators,
currentQuery,
} = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const isSavedViewEnabled = useSavedViewEnabled();
const getExportToDashboardLink = useGetExportToDashboardLink();
const { handleRunQuery, updateAllQueriesOperators } = useQueryBuilder();
const queryClient = useQueryClient();
const [isLoadingQueries, setIsLoadingQueries] = useState(false);
const [isCancelled, setIsCancelled] = useState(false);
@@ -70,51 +54,8 @@ function Explorer(): JSX.Element {
[updateAllQueriesOperators],
);
const exportDefaultQuery = useMemo(
() =>
updateAllQueriesOperators(
currentQuery || initialQueryMeterWithType,
PANEL_TYPES.BAR,
DataSource.METRICS,
'meter' as 'meter' | '',
),
[currentQuery, updateAllQueriesOperators],
);
useShareBuilderUrl({ defaultValue: defaultQuery });
const handleExport = useCallback(
(
dashboard: ExportDashboard | null,
_isNewDashboard?: boolean,
queryToExport?: Query,
): void => {
if (!dashboard) {
return;
}
const widgetId = uuid();
const dashboardEditView = getExportToDashboardLink({
query: queryToExport || exportDefaultQuery,
panelType: PANEL_TYPES.BAR,
dashboardId: dashboard.id,
widgetId,
});
if (dashboardEditView) {
safeNavigate(dashboardEditView);
}
},
[exportDefaultQuery, safeNavigate, getExportToDashboardLink],
);
const splitedQueries = useMemo(
() =>
splitQueryIntoOneChartPerQuery(stagedQuery || initialQueryMeterWithType),
[stagedQuery],
);
useEffect(() => {
logEvent(MeterExplorerEvents.TabChanged, {
[MeterExplorerEventKeys.Tab]: 'explorer',
@@ -178,17 +119,6 @@ function Explorer(): JSX.Element {
/>
</div>
</div>
{!isSavedViewEnabled && (
<ExplorerOptionWrapper
disabled={!stagedQuery}
query={exportDefaultQuery}
sourcepage={DataSource.METRICS}
signalSource="meter"
onExport={handleExport}
isOneChartPerQuery={false}
splitedQueries={splitedQueries}
/>
)}
</div>
</QuickFiltersLayout>
</Sentry.ErrorBoundary>

View File

@@ -1,37 +0,0 @@
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
export const splitQueryIntoOneChartPerQuery = (query: Query): Query[] => {
const queries: Query[] = [];
query.builder.queryData.forEach((currentQuery) => {
const newQuery = {
...query,
id: uuid(),
builder: {
...query.builder,
queryData: [currentQuery],
queryFormulas: [],
},
};
queries.push(newQuery);
});
query.builder.queryFormulas.forEach((currentFormula) => {
const newQuery = {
...query,
id: uuid(),
builder: {
...query.builder,
queryFormulas: [currentFormula],
queryData: query.builder.queryData.map((currentQuery) => ({
...currentQuery,
disabled: true,
})),
},
};
queries.push(newQuery);
});
return queries;
};

View File

@@ -1,6 +1,4 @@
.metrics-explorer-explore-container {
padding-bottom: 80px;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);

View File

@@ -14,20 +14,11 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { usePageActions } from 'container/AIAssistant/pageActions/usePageActions';
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
import { useGetExportToDashboardLink } from 'hooks/dashboard/useGetExportToDashboardLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import {
ICurrentQueryData,
useHandleExplorerTabChange,
} from 'hooks/useHandleExplorerTabChange';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useSavedViewEnabled } from 'hooks/useSavedViewEnabled';
import { isEmpty } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import {
@@ -35,13 +26,9 @@ import {
metricsRunQueryAction,
metricsSaveViewAction,
} from 'pages/MetricsExplorer/aiActions';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { Warning } from 'types/api';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import { explorerViewToPanelType } from 'utils/explorerUtils';
import { v4 as uuid } from 'uuid';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import MetricDetails from '../MetricDetails/MetricDetails';
@@ -62,14 +49,9 @@ function Explorer(): JSX.Element {
stagedQuery,
updateAllQueriesOperators,
currentQuery,
handleSetConfig,
handleSetQueryData,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const isSavedViewEnabled = useSavedViewEnabled();
const getExportToDashboardLink = useGetExportToDashboardLink();
const { handleExplorerTabChange } = useHandleExplorerTabChange();
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(false);
const [showSidebar, setShowSidebar] = useState(true);
@@ -220,16 +202,6 @@ function Explorer(): JSX.Element {
useShareBuilderUrl({ defaultValue: defaultQuery });
const handleChangeSelectedView = useCallback(
(view: ExplorerViews, querySearchParameters?: ICurrentQueryData): void => {
const nextPanelType =
explorerViewToPanelType[view] || PANEL_TYPES.TIME_SERIES;
handleSetConfig(nextPanelType, DataSource.METRICS);
handleExplorerTabChange(nextPanelType, querySearchParameters);
},
[handleSetConfig, handleExplorerTabChange],
);
// ─── AI Assistant page actions (only when license feature is on) ───────────
const aiActions = useMemo(
() =>
@@ -265,40 +237,6 @@ function Explorer(): JSX.Element {
usePageActions('metrics-explorer', aiActions);
// ───────────────────────────────────────────────────────────────────────────
const handleExport = useCallback(
(
dashboard: ExportDashboard | null,
_isNewDashboard?: boolean,
queryToExport?: Query,
): void => {
if (!dashboard) {
return;
}
const widgetId = uuid();
let query = queryToExport || exportDefaultQuery;
if (yAxisUnit && !query.unit) {
query = {
...query,
unit: yAxisUnit,
};
}
const dashboardEditView = getExportToDashboardLink({
query,
panelType: PANEL_TYPES.TIME_SERIES,
dashboardId: dashboard.id,
widgetId,
});
if (dashboardEditView) {
safeNavigate(dashboardEditView);
}
},
[exportDefaultQuery, safeNavigate, yAxisUnit, getExportToDashboardLink],
);
const splitedQueries = useMemo(
() =>
splitQueryIntoOneChartPerQuery(
@@ -424,17 +362,6 @@ function Explorer(): JSX.Element {
</div>
</div>
</QuickFiltersLayout>
{!isSavedViewEnabled && (
<ExplorerOptionWrapper
disabled={!stagedQuery}
query={exportDefaultQuery}
sourcepage={DataSource.METRICS}
onExport={handleExport}
isOneChartPerQuery={showOneChartPerQuery}
splitedQueries={splitedQueries}
handleChangeSelectedView={handleChangeSelectedView}
/>
)}
{isMetricDetailsOpen && selectedMetricName && (
<MetricDetails
metricName={selectedMetricName}

View File

@@ -39,7 +39,6 @@ const mockUseQueryBuilderData = {
currentQuery: initialQueriesMap[DataSource.METRICS],
resetQuery: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(),
handleSetQueryData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),

View File

@@ -68,7 +68,6 @@ const mockUseQueryBuilderData = {
currentQuery: initialQueriesMap[DataSource.METRICS],
resetQuery: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
isStagedQueryUpdated: jest.fn(),
handleSetQueryData: jest.fn(),
handleSetFormulaData: jest.fn(),
handleSetQueryItemData: jest.fn(),

View File

@@ -20,10 +20,7 @@ export enum MetricsExplorerEvents {
AddToAlertClicked = 'Metrics Explorer: Add to alert clicked',
AddToDashboardClicked = 'Metrics Explorer: Add to dashboard clicked',
AddToDashboardSuccessful = 'Metrics Explorer: Add to dashboard successful',
SaveViewClicked = 'Metrics Explorer: Save view clicked',
SearchApplied = 'Metrics Explorer: Search applied',
ViewEdited = 'Metrics Explorer: View edited',
ViewDeleted = 'Metrics Explorer: View deleted',
}
export enum MetricsExplorerEventKeys {
@@ -46,7 +43,6 @@ export enum MetricsExplorerEventKeys {
SpaceAggregationLabels = 'spaceAggregationLabels',
OneChartPerQueryEnabled = 'oneChartPerQueryEnabled',
YAxisUnit = 'yAxisUnit',
ViewName = 'viewName',
Filters = 'filters',
TimeRange = 'timeRange',
}

View File

@@ -25,6 +25,7 @@ import azureVmUrl from '@/assets/Logos/azure-vm.svg';
import azureUrl from '@/assets/Logos/azure.svg';
import basetenUrl from '@/assets/Logos/baseten.svg';
import bunUrl from '@/assets/Logos/bun.svg';
import caddyUrl from '@/assets/Logos/caddy.svg';
import cassandraUrl from '@/assets/Logos/cassandra.svg';
import celeryUrl from '@/assets/Logos/celery.svg';
import certManagerUrl from '@/assets/Logos/cert-manager.svg';
@@ -96,6 +97,7 @@ import haystackUrl from '@/assets/Logos/haystack.svg';
import hcpVaultUrl from '@/assets/Logos/hcp-vault.svg';
import herokuUrl from '@/assets/Logos/heroku.svg';
import honeycombUrl from '@/assets/Logos/honeycomb.svg';
import honoUrl from '@/assets/Logos/hono.svg';
import hostmetricsUrl from '@/assets/Logos/hostmetrics.svg';
import httpUrl from '@/assets/Logos/http.svg';
import httpMonitoringUrl from '@/assets/Logos/http-monitoring.svg';
@@ -152,6 +154,7 @@ import phpUrl from '@/assets/Logos/php.svg';
import pinoUrl from '@/assets/Logos/pino.svg';
import pipecatUrl from '@/assets/Logos/pipecat.svg';
import planetscaleUrl from '@/assets/Logos/planetscale.svg';
import pm2Url from '@/assets/Logos/pm2.svg';
import podmanUrl from '@/assets/Logos/podman.svg';
import postgresqlUrl from '@/assets/Logos/postgresql.svg';
import prometheusUrl from '@/assets/Logos/prometheus.svg';
@@ -7239,13 +7242,15 @@ const onboardingConfigWithLinks = [
dataSource: 'gcp-integration',
label: 'GCP Integration',
imgUrl: gcpUrl,
tags: ['GCP'],
module: 'metrics',
tags: ['GCP', 'logs'],
module: 'integrations',
relatedSearchKeywords: [
'cloud logging',
'cloud monitoring metrics',
'connect gcp account',
'gcp',
'gcp integration',
'gcp logs',
'gcp manual setup',
'gcp metrics',
'gcp monitoring',
@@ -7253,10 +7258,13 @@ const onboardingConfigWithLinks = [
'gcp service account',
'google cloud',
'google cloud integration',
'log sink',
'logs',
'metrics',
'monitoring',
'observability',
'opentelemetry collector gcp',
'pub/sub',
],
id: 'gcp-integration',
link: '/docs/integrations/gcp/gcp-integration/',
@@ -8066,5 +8074,110 @@ const onboardingConfigWithLinks = [
id: 'microsoft-agent-framework',
link: '/docs/microsoft-agent-framework-observability/',
},
{
dataSource: 'hono',
label: 'Hono',
imgUrl: honoUrl,
tags: ['apm/traces', 'metrics'],
module: 'apm',
relatedSearchKeywords: [
'apm',
'bun hono',
'hono',
'hono metrics',
'hono monitoring',
'hono observability',
'hono traces',
'honojs',
'javascript',
'metrics',
'nodejs',
'nodejs hono',
'observability',
'opentelemetry hono',
'traces',
'tracing',
'typescript',
],
id: 'hono',
link: '/docs/instrumentation/javascript/opentelemetry-hono/',
},
{
dataSource: 'caddy',
label: 'Caddy',
imgUrl: caddyUrl,
tags: ['apm/traces', 'metrics', 'logs'],
module: 'apm',
relatedSearchKeywords: [
'access logs',
'apm',
'caddy',
'caddy logs',
'caddy metrics',
'caddy monitoring',
'caddy server',
'caddy traces',
'logs',
'metrics',
'observability',
'opentelemetry caddy',
'reverse proxy',
'traces',
'tracing',
'web server',
],
id: 'caddy',
link: '/docs/integrations/opentelemetry-caddy/',
},
{
dataSource: 'pm2',
label: 'PM2',
imgUrl: pm2Url,
tags: ['apm/traces'],
module: 'apm',
relatedSearchKeywords: [
'apm',
'cluster mode',
'javascript',
'nodejs',
'nodejs pm2',
'observability',
'opentelemetry pm2',
'pm2',
'pm2 cluster',
'pm2 monitoring',
'pm2 traces',
'process manager',
'traces',
'tracing',
],
id: 'pm2',
link: '/docs/instrumentation/javascript/opentelemetry-pm2/',
},
{
dataSource: 'gcp-one-click-integration',
label: 'GCP One-Click Integration',
imgUrl: gcpUrl,
tags: ['GCP', 'metrics', 'logs'],
module: 'integrations',
relatedSearchKeywords: [
'gcp',
'gcp agent',
'gcp integration',
'gcp logs',
'gcp metrics',
'gcp one click',
'google cloud',
'google cloud integration',
'logs',
'metrics',
'monitoring',
'observability',
'one click',
'one click gcp integration',
],
id: 'gcp-one-click-integration',
link: '/docs/integrations/gcp/one-click-gcp-integrations/',
},
];
export default onboardingConfigWithLinks;

View File

@@ -4,6 +4,7 @@ import {
SavedviewtypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import {
buildSavedViewDeletePermission,
buildSavedViewUpdatePermission,
@@ -30,6 +31,15 @@ export const SAVED_VIEW_URL_PARAMS = [
QueryParams.panelTypes,
];
export const SOURCEPAGE_VS_ROUTES: {
[key: string]: Partial<(typeof ROUTES)[keyof typeof ROUTES]>;
} = {
logs: ROUTES.LOGS_EXPLORER,
traces: ROUTES.TRACES_EXPLORER,
metrics: ROUTES.METRICS_EXPLORER_EXPLORER,
meter: ROUTES.METER_EXPLORER,
} as const;
export const MY_VIEW_NAME = 'My view';
export const SAVED_VIEW_LOAD_FAILED_NAME = 'View failed to load';

View File

@@ -6,9 +6,8 @@ import { LOCALSTORAGE } from 'constants/localStorage';
import {
PreservedViewsInLocalStorage,
PreservedViewType,
} from 'container/ExplorerOptions/types';
import { UseLastUsedViewResult } from '../types';
UseLastUsedViewResult,
} from '../types';
function readLastUsedViews(): PreservedViewsInLocalStorage {
try {

View File

@@ -225,3 +225,20 @@ export interface SavedViewsEmptyStateProps {
isSaving: boolean;
onSave: (displayName: string) => Promise<boolean>;
}
export enum PreservedViewsTypes {
LOGS = 'logs',
TRACES = 'traces',
METRICS = 'metrics',
METER = 'meter',
}
export type PreservedViewType =
| PreservedViewsTypes.LOGS
| PreservedViewsTypes.TRACES
| PreservedViewsTypes.METRICS
| PreservedViewsTypes.METER;
export type PreservedViewsInLocalStorage = Partial<
Record<PreservedViewType, { key: string; value: string }>
>;

View File

@@ -166,10 +166,8 @@ export const routesToSkip = [
ROUTES.LOGS,
ROUTES.MY_SETTINGS,
ROUTES.LIST_LICENSES,
ROUTES.LOGS_SAVE_VIEWS,
ROUTES.LOGS_PIPELINES,
ROUTES.TRACES_EXPLORER,
ROUTES.TRACES_SAVE_VIEWS,
ROUTES.TRACES_FUNNELS,
ROUTES.TRACES_FUNNELS_DETAIL,
ROUTES.SHORTCUTS,
@@ -188,7 +186,6 @@ export const routesToSkip = [
ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
ROUTES.METRICS_EXPLORER,
ROUTES.METRICS_EXPLORER_EXPLORER,
ROUTES.METRICS_EXPLORER_VIEWS,
ROUTES.API_MONITORING,
ROUTES.CHANNELS_NEW,
ROUTES.CHANNELS_EDIT,
@@ -198,7 +195,6 @@ export const routesToSkip = [
ROUTES.NOT_FOUND,
ROUTES.METER_EXPLORER,
ROUTES.METER,
ROUTES.METER_EXPLORER_VIEWS,
ROUTES.METRICS_EXPLORER_VOLUME_CONTROL,
ROUTES.SOMETHING_WENT_WRONG,
ROUTES.AI_OBSERVABILITY_OVERVIEW,

View File

@@ -6,8 +6,7 @@ import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
jest.mock('hooks/queryBuilder/useQueryBuilder');
jest.mock('hooks/saveViews/useGetSavedViewParams', () => ({
useGetSavedViewParams: (): { viewName: string; viewKey: string } => ({
viewName: '',
useGetSavedViewParams: (): { viewKey: string } => ({
viewKey: '',
}),
}));

View File

@@ -21,40 +21,30 @@ describe('useGetSavedViewParams', () => {
const { result } = renderHook(() => useGetSavedViewParams());
expect(result.current).toStrictEqual({ viewName: '', viewKey: '' });
expect(result.current).toStrictEqual({ viewKey: '' });
});
it('parses JSON-stringified values', () => {
setSearch(
`viewName=${encodeURIComponent(
JSON.stringify('Hindsight'),
)}&viewKey=${encodeURIComponent(JSON.stringify('abc-123'))}`,
);
setSearch(`viewKey=${encodeURIComponent(JSON.stringify('abc-123'))}`);
const { result } = renderHook(() => useGetSavedViewParams());
expect(result.current).toStrictEqual({
viewName: 'Hindsight',
viewKey: 'abc-123',
});
expect(result.current).toStrictEqual({ viewKey: 'abc-123' });
});
it('falls back to the raw string when a value is not valid JSON', () => {
setSearch('viewName=Hindsight&viewKey=some-uuid-value');
setSearch('viewKey=some-uuid-value');
const { result } = renderHook(() => useGetSavedViewParams());
expect(result.current).toStrictEqual({
viewName: 'Hindsight',
viewKey: 'some-uuid-value',
});
expect(result.current).toStrictEqual({ viewKey: 'some-uuid-value' });
});
it('does not throw and keeps the raw string for non-string JSON', () => {
setSearch('viewName=123');
setSearch('viewKey=123');
const { result } = renderHook(() => useGetSavedViewParams());
expect(result.current).toStrictEqual({ viewName: '123', viewKey: '' });
expect(result.current).toStrictEqual({ viewKey: '123' });
});
});

View File

@@ -1,18 +0,0 @@
import { useMutation, UseMutationResult, useQueryClient } from 'react-query';
import { invalidateListSavedViews } from 'api/generated/services/saved-view';
import { deleteView } from 'api/saveView/deleteView';
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
export const useDeleteView = (
uuid: string,
): UseMutationResult<DeleteViewPayloadProps, Error, string> => {
const queryClient = useQueryClient();
return useMutation({
mutationKey: [uuid],
mutationFn: () => deleteView(uuid),
// v1 and v2 share storage; consumers already on v2 must see this write.
// Temporary till the v1 client is deleted with the explorer bar.
onSuccess: () => invalidateListSavedViews(queryClient),
});
};

View File

@@ -1,15 +0,0 @@
import { useQuery, UseQueryResult } from 'react-query';
import { getAllViews } from 'api/saveView/getAllViews';
import { AxiosError, AxiosResponse } from 'axios';
import { AllViewsProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
export const useGetAllViews = (
sourcepage: DataSource | 'meter',
enabled?: boolean,
): UseQueryResult<AxiosResponse<AllViewsProps>, AxiosError> =>
useQuery<AxiosResponse<AllViewsProps>, AxiosError>({
queryKey: [{ sourcepage }],
queryFn: () => getAllViews(sourcepage as DataSource),
...(enabled !== undefined ? { enabled } : {}),
});

View File

@@ -3,7 +3,6 @@ import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
interface SavedViewParams {
viewName: string;
viewKey: string;
}
@@ -25,7 +24,6 @@ export const useGetSavedViewParams = (): SavedViewParams => {
return useMemo(
() => ({
viewName: parseViewParam(urlQuery.get(QueryParams.viewName)),
viewKey: parseViewParam(urlQuery.get(QueryParams.viewKey)),
}),
[urlQuery],

View File

@@ -1,27 +0,0 @@
import { useMutation, UseMutationResult, useQueryClient } from 'react-query';
import { invalidateListSavedViews } from 'api/generated/services/saved-view';
import { saveView } from 'api/saveView/saveView';
import { AxiosResponse } from 'axios';
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
export const useSaveView = ({
compositeQuery,
sourcePage,
viewName,
extraData,
}: SaveViewProps): UseMutationResult<
AxiosResponse<SaveViewPayloadProps>,
Error,
SaveViewProps,
SaveViewPayloadProps
> => {
const queryClient = useQueryClient();
return useMutation({
mutationKey: [viewName, sourcePage, compositeQuery, extraData],
mutationFn: saveView,
// v1 and v2 share storage; consumers already on v2 must see this write.
// Temporary till the v1 client is deleted with the explorer bar.
onSuccess: () => invalidateListSavedViews(queryClient),
});
};

View File

@@ -1,37 +0,0 @@
import { useMutation, UseMutationResult, useQueryClient } from 'react-query';
import { invalidateListSavedViews } from 'api/generated/services/saved-view';
import { updateView } from 'api/saveView/updateView';
import {
UpdateViewPayloadProps,
UpdateViewProps,
} from 'types/api/saveViews/types';
export const useUpdateView = ({
compositeQuery,
viewName,
extraData,
sourcePage,
viewKey,
}: UpdateViewProps): UseMutationResult<
UpdateViewPayloadProps,
Error,
UpdateViewProps,
UpdateViewPayloadProps
> => {
const queryClient = useQueryClient();
return useMutation({
mutationKey: [viewName, sourcePage, compositeQuery, extraData],
mutationFn: () =>
updateView({
compositeQuery,
viewName,
extraData,
sourcePage,
viewKey,
}),
// v1 and v2 share storage; consumers already on v2 must see this write.
// Temporary till the v1 client is deleted with the explorer bar.
onSuccess: () => invalidateListSavedViews(queryClient),
});
};

View File

@@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { QueryParams } from 'constants/query';
import { initialAutocompleteData, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
@@ -9,11 +10,15 @@ import { useGetSavedViewParams } from './saveViews/useGetSavedViewParams';
import { useQueryBuilder } from './queryBuilder/useQueryBuilder';
export interface ICurrentQueryData {
viewName?: string;
viewKey?: string;
query: Query;
}
export type ChangeViewFunctionType = (
view: ExplorerViews,
querySearchParameters?: ICurrentQueryData,
) => void;
export const useHandleExplorerTabChange = (): {
handleExplorerTabChange: (
type: string,
@@ -30,7 +35,7 @@ export const useHandleExplorerTabChange = (): {
updateQueriesData,
} = useQueryBuilder();
const { viewName, viewKey } = useGetSavedViewParams();
const { viewKey } = useGetSavedViewParams();
const getUpdateQuery = useCallback(
(newPanelType: PANEL_TYPES): Query => {
@@ -73,21 +78,19 @@ export const useHandleExplorerTabChange = (): {
const query = currentQueryData?.query || getUpdateQuery(newPanelType);
const nextViewKey = currentQueryData?.viewKey || viewKey;
const nextViewName = currentQueryData?.viewName || viewName;
redirectWithQueryBuilderData(
query,
{
[QueryParams.panelTypes]: newPanelType,
...(nextViewKey ? { [QueryParams.viewKey]: nextViewKey } : {}),
...(nextViewName ? { [QueryParams.viewName]: nextViewName } : {}),
},
redirectToUrl,
undefined,
newTab,
);
},
[panelType, getUpdateQuery, redirectWithQueryBuilderData, viewName, viewKey],
[panelType, getUpdateQuery, redirectWithQueryBuilderData, viewKey],
);
return { handleExplorerTabChange };

View File

@@ -1,3 +0,0 @@
export function useSavedViewEnabled(): boolean {
return true;
}

View File

@@ -1,133 +0,0 @@
import { ReduceOperators } from 'types/common/queryBuilder';
export const explorerView = {
status: 'success',
data: [
{
id: 'test-uuid-1',
name: 'Table View',
category: '',
createdAt: '2023-08-29T18:04:10.906310033Z',
createdBy: 'test-user-1',
updatedAt: '2024-01-29T10:42:47.346331133Z',
updatedBy: 'test-user-1',
sourcePage: 'traces',
tags: [''],
compositeQuery: {
builderQueries: {
A: {
queryName: 'A',
stepInterval: 60,
dataSource: 'traces',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'component',
dataType: 'string',
type: 'tag',
},
filters: {
op: 'AND',
items: [
{
key: {
key: 'component',
dataType: 'string',
type: 'tag',
},
value: 'test-component',
op: '!=',
},
],
},
groupBy: [
{
key: 'component',
dataType: 'string',
type: 'tag',
},
{
key: 'client-uuid',
dataType: 'string',
type: 'resource',
},
],
expression: 'A',
disabled: false,
limit: 0,
offset: 0,
pageSize: 0,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
reduceTo: ReduceOperators.SUM,
ShiftBy: 0,
},
},
panelType: 'table',
queryType: 'builder',
},
extraData: '{"color":"#00ffd0"}',
},
{
id: '8c4bf492-d54d-4ab2-a8d6-9c1563f46e1f',
name: 'R-test panel',
category: '',
createdAt: '2024-07-01T13:45:57.924686766Z',
createdBy: 'test-user-test',
updatedAt: '2024-07-01T13:48:31.032106578Z',
updatedBy: 'test-user-test',
sourcePage: 'traces',
tags: [''],
compositeQuery: {
builderQueries: {
A: {
queryName: 'A',
stepInterval: 60,
dataSource: 'traces',
aggregateOperator: 'noop',
aggregateAttribute: {
key: '',
dataType: '',
type: '',
},
filters: {
op: 'AND',
items: [
{
key: {
key: 'httpMethod',
dataType: 'string',
type: 'tag',
},
value: 'GET',
op: '=',
},
],
},
expression: 'A',
disabled: false,
limit: 0,
offset: 0,
pageSize: 0,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
reduceTo: ReduceOperators.AVG,
timeAggregation: 'rate',
spaceAggregation: 'sum',
ShiftBy: 0,
},
},
panelType: 'list',
queryType: 'builder',
},
extraData: '{"color":"#AD7F58"}',
},
],
};

View File

@@ -13,7 +13,6 @@ import {
dashboardSuccessResponse,
getDashboardById,
} from './__mockdata__/dashboards';
import { explorerView } from './__mockdata__/explorer_views';
import { licensesSuccessResponse } from './__mockdata__/licenses';
import { queryRangeSuccessResponse } from './__mockdata__/query_range';
import { serviceSuccessResponse } from './__mockdata__/services';
@@ -175,20 +174,6 @@ export const handlers = [
),
),
rest.get('http://localhost/api/v1/explorer/views', (req, res, ctx) =>
res(ctx.status(200), ctx.json(explorerView)),
),
rest.post('http://localhost/api/v1/explorer/views', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: '7731ece1-3fa3-4ed4-8b1c-58b4c28723b2',
}),
),
),
rest.post('http://localhost/api/v1/event', (req, res, ctx) =>
res(
ctx.status(200),

View File

@@ -304,14 +304,6 @@ export const TooltipsInApprovalDiff: Story = {
play: openApprovalDiff,
};
/**
* The conversation count in the bottom strip, in place of the build version.
* Archived threads are left out of it.
*/
export const BottomStrip: Story = {
args: { bottomStrip: true },
};
/**
* The recording controls' tooltips, held open: discard, stop and send, and the
* send button beside them, which waits for words.

View File

@@ -136,16 +136,11 @@ export const Tooltips: Story = {
};
/**
* The rule count in the bottom strip, in place of the build version: the rows on
* the page against the total, the same pair the table's own footer prints.
* The rule count in the bottom strip on a second page, where the rows on the
* page and the total come apart.
*/
export const BottomStrip: Story = {
args: { bottomStrip: true },
};
/** The same count on a second page, where the two numbers come apart. */
export const BottomStripPaginated: Story = {
args: { bottomStrip: true, rules: RULE_MAX },
args: { rules: RULE_MAX },
parameters: {
signoz: { route: '/alerts?tab=AlertRules&page=2&limit=10' },
},

View File

@@ -131,14 +131,6 @@ export const QuickFiltersSettingsWithBanner: Story = {
play: dirtyQuickFiltersSettings,
};
/**
* The exception count in the bottom strip, in place of the build version: the
* rows on the page against the total the count query returns.
*/
export const BottomStrip: Story = {
args: { bottomStrip: true },
};
/** The resource filter opened: every key the exceptions can be narrowed by. */
export const FilterKeySuggestions: Story = {
play: async ({ canvasElement }): Promise<void> => {

View File

@@ -97,11 +97,3 @@ export const NavSettingsMenu: Story = {
await screen.findByRole('menu');
},
};
/**
* The firing alert count in the bottom strip, in place of the build version. It
* counts firing instances, so it does not match the alert rules widget above.
*/
export const BottomStrip: Story = {
args: { bottomStrip: true },
};

View File

@@ -28,7 +28,6 @@ import {
// same endpoints for the same shapes and answers from the traces builders.
import {
exportDashboardsResponse,
savedTraceViewsResponse,
traceAttributeValuesResponse,
traceFieldKeysResponse,
traceFieldValues,
@@ -41,7 +40,6 @@ import {
const VIEW = 'Explorer · view';
const LIST = 'Explorer · list';
const FILTERS = 'Explorer · filters';
const SAVED = 'Explorer · saved';
const explorerRoute = (view: ExplorerViews): string => {
const params = new URLSearchParams({
@@ -97,12 +95,6 @@ export const llmExplorerMocks = defineStoryMocks({
value: 6,
max: 8,
}),
savedViews: countControl('Saved views', {
group: SAVED,
description: 'Fills the views dropdown the explorer options offer.',
value: 4,
max: 8,
}),
},
handlers: (values, response) => [
rest.get(
@@ -145,11 +137,6 @@ export const llmExplorerMocks = defineStoryMocks({
),
),
rest.get(
'http://localhost/api/v1/explorer/views',
response.json(() => savedTraceViewsResponse(values.savedViews)),
),
rest.get(
'http://localhost/api/v2/users/me/dashboards',
response.json(exportDashboardsResponse),

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