Compare commits

..

8 Commits

Author SHA1 Message Date
Abhi Kumar
18db419644 refactor(widgets): split types/api/dashboard/getAll into widget and variable types
getAll.ts was the last place the V1 dashboard entity and the chart-spec types
shared a home. Its 155 importers were overwhelmingly reaching for `Widgets`,
which has nothing to do with the dashboards API: ~49 of them are APM, Celery,
Messaging Queues, API Monitoring and the query/uPlot libs.

  Widgets, IBaseWidget, LegendPosition, ContextLink*  -> types/api/widgets/widget
  IDashboardVariable, TVariableQueryType, Variable*   -> types/api/dashboard/variables

Everything else in the file was dead and is deleted with it: Dashboard,
DashboardData, WidgetRow, PayloadProps, DashboardTemplate, PromQLWidgets and
IQueryBuilderTagFilterItems. The last reference to the Dashboard entity was
hasColumnWidthsChanged, which became unreachable when the V1 store went — it
compared against store column widths nothing wrote.

types/api/dashboard/ now holds only variable types, and no V1 dashboard entity
type exists anywhere in the frontend.
2026-08-21 23:49:59 +05:30
Abhi Kumar
a6e09083d7 refactor(widgets): consolidate the widget-card stack under container/WidgetCard
Rendering a V1 `Widgets` spec as a self-contained chart card was spread across
five sibling directories named after the old dashboard grid, even though the
dashboard grid is gone and the remaining consumers are APM, Celery, Messaging
Queues, API Monitoring and Meter Explorer. Group them under one name that says
what they do.

  container/GridCardLayout/GridCard      -> container/WidgetCard/Card
  container/GridCardLayout/WidgetHeader  -> container/WidgetCard/Header
  container/GridCardLayout/EmptyWidget   -> container/WidgetCard/EmptyWidget
  container/GridCardLayout/use*.ts       -> container/WidgetCard/hooks/
  container/PanelWrapper                 -> container/WidgetCard/PanelWrapper
  container/GridTableComponent           -> container/WidgetCard/TablePanel
  container/GridValueComponent           -> container/WidgetCard/ValuePanel

container/GridPanelSwitch is gone: its index/types were already unreferenced,
and generateGridTitle — a ReactNode-to-string helper with no widget-card
connection — moves to utils/.

Also closes the last two lib -> container back-edges left by the chart-layer
move. CustomCheckBox lived in the card's FullView tree but its only consumer is
ChartManager, so it moves into lib/visualization beside it, and the
ExtendedChartDataset type it needs joins the other chart types.
lib/visualization now imports nothing from container/.
2026-08-21 21:23:09 +05:30
Abhi Kumar
e97d127af2 refactor(charts): move the shared chart layer to lib/visualization
The chart layer lived under container/DashboardContainer/visualization even
though twelve areas import it — V2 dashboards, Alerts, Billing, Infra K8s,
Metrics and Meter Explorer, API Monitoring, TimeSeriesView and lib/uPlotV2 —
and nothing V1-dashboard-specific was left around it. Move it next to
lib/uPlotV2, the chart foundation it builds on.

  container/DashboardContainer/visualization -> lib/visualization

Only the reusable half goes to lib. The three V1 panel adapters (Bar,
TimeSeries, Histogram) and usePanelContextMenu implement container/PanelWrapper's
interface and are reachable only through it, so they move there instead:

  lib/visualization/panels/{Bar,TimeSeries,Histogram}Panel
    -> container/PanelWrapper/panels/
  lib/visualization/hooks/usePanelContextMenu
    -> container/PanelWrapper/hooks/

That drops the lib -> container back-edges from 9 to 1 (a CustomCheckbox import
that resolves when the widget-card stack moves).

Splitting them also required splitting container/PanelWrapper/constants, which
mixed the panel-type registry with plain visualization constants. The latter
(DEFAULT_BUCKET_COUNT, histogramBucketSizes, NULL_*) now live in
lib/visualization/constants, which additionally removes a V2 -> V1 PanelWrapper
dependency from the V2 histogram panel.

container/DashboardContainer is now empty and deleted: no V1 dashboard
container remains.
2026-08-21 20:41:54 +05:30
Abhi Kumar
fbbceffbeb chore(dashboard): retire the V1 dashboard store
Nothing has written `dashboardData` since the V1 page was removed, so every
read of it returned undefined and every value derived from it was effectively a
constant. Replace those reads with the values they already had, then delete the
store.

- useCreateAlerts: drop `dashboardName`/`dashboardId` from its logEvent
  payloads; both were always undefined and neither affected the created alert.
- Celery explorer nav and the aggregate drilldown: stop passing `dashboardData`
  into getUpdatedQuery, where it only fed getDashboardVariables(undefined).
- useNavigateToExplorerPages: the read appeared only in a dep array.
- FullView: `version` was always 'v3' and the graph was never locked.
- FullView / WidgetGraphComponent: drop the column-width writes. Nothing read
  store.columnWidths — every consumer reads widget.columnWidths or a prop.
- TimeSeries/Bar panels: with no dashboard id there is no stored cursor-sync
  preference, so derive syncMode from the panel-mode gate alone and pass the
  default tooltip filter mode. Behaviour is unchanged: DASHBOARD_VIEW keeps
  Crosshair, STANDALONE_VIEW keeps None.
- ChartManager: the dashboard was never locked, so isGraphDisabled is false.

Also removes the hooks left unreferenced once the V1 widget page went
(useTransformDashboardVariables, useVariablesFromUrl,
useDashboardFromLocalStorage, normalizeUrlValue), which knip cannot flag
because their own tests kept them reachable.

Unrelated flake fixed on the way: QuerySearch's mount test waited on "any"
key-suggestion call, so a debounced fetch leaking from an earlier test could
satisfy it and the assertion then depended on jest's file ordering. It now
waits for the mount call itself.
2026-08-21 20:23:34 +05:30
Abhi Kumar
74bbcb20d5 feat(dashboard): show a legacy notice for unmigrated public dashboards
Public dashboards whose stored data never reached v6 were rendered by the V1
container, the last V1 dashboard renderer in the app. Replace it with a notice
so there is a single rendering path.

- useGetResolvedPublicDashboard resolves to a `Legacy` marker on the v2
  endpoint's `dashboard_invalid_data` code instead of fetching v1. Any other v2
  error still propagates, so a transient failure is not reported as "legacy".
- Extract the page's branded full-page state into PublicDashboardMessage and
  render it for both the legacy and unavailable cases. Public viewers are
  anonymous, so the notice points at the dashboard owner rather than offering
  retry-migration or support actions.
- Delete container/PublicDashboardContainer and the v1 public data APIs.
- Panel.tsx was the only producer of `publicQueryMeta`, so drop that parameter
  and its branch from GetMetricQueryRange and useGetQueryRange. Removing it
  left `isInfraMonitoring` trailing and unused (it already was, just not
  reported) so that goes too; no caller passed either.

Behaviour change: a viewer of an unmigrated public dashboard now sees the
notice instead of charts, until an admin re-runs the migration.
2026-08-21 19:27:50 +05:30
Abhi Kumar
bdb8b6a675 chore(dashboard): retire the V1 panel editor and its widget route
`ROUTES.DASHBOARD_WIDGET` (`/dashboard/:dashboardId/:widgetId`) was still
registered but no UI linked to it, and its page fetched
`GET /api/v1/dashboards/{id}`, which the backend answers 501. V2 serves panel
editing at `/dashboard/:dashboardId/panel/:panelId`.

Extract what other features still need out of container/NewWidget first, then
delete the route, the page and the container.

Rehomed:
- RightContainer/Threshold/types    -> types/api/widgets/threshold
- RightContainer/types + format categories -> constants/formats/*
  (fixing the alertFomatCategories spelling)
- RightContainer/timeItems          -> constants/timePreference
- RightContainer/ContextLinks/*     -> utils/contextLinks/*
- LeftContainer/QueryTypeTag        -> components/QueryTypeTag
- LeftContainer/WidgetGraph/PlotTag -> components/PlotTag
- LeftContainer/WidgetGraph/util    -> lib/query/populateMultipleResults
- QuerySection/QueryBuilder/{promQL,ClickHouse} ->
  container/QueryBuilder/rawQueryEditors/{PromQL,ClickHouse}
- the four externally-used utils exports -> lib/query/panelQuery

Also:
- Meter Explorer's "Add to dashboard" built a V1 editor URL, so it landed on
  the 501 page. It now uses useGetExportToDashboardLink like the logs, traces
  and metrics explorers, and generateExportToDashboardLink is gone.
- Drop the FullView "Switch to Edit Mode" button and the WidgetHeader
  Edit/Delete/Clone items: all were gated on V1 state nothing populates, so
  they never rendered.
- Delete the V1 write path (useUpdateDashboard, api/v1/dashboards/id/update)
  and the orphaned bootstrap chain.
- Extract ColumnUnit to types/api/widgets/columnUnit to break the
  getAll <-> threshold import cycle the type move would otherwise have created.
2026-08-21 19:02:55 +05:30
Abhi Kumar
b5a1fb2106 chore(dashboard): remove dead V1 dashboard code
V2 is the only implementation serving /dashboard and /dashboard/:id, so
the V1 page bodies and everything reachable only from them are dead.
Remove that cluster and rehome the pieces other features still use.

- point the routes straight at the V2 pages and drop the V1 shims
- delete container/ListOfDashboard, the non-visualization half of
  container/DashboardContainer, and the GridCardLayout grid shell
- rescue shared code before its folder went: the variable dependency
  graph to lib/dashboardVariables/dependencyGraph.ts, uniqueOptions into
  NewSelect, panel-type items to GridCardLayout/panelTypeItems.tsx
- drop two unreachable menu affordances: panel Delete/Clone, which no
  live caller lists in headerMenuList, and the Dashboard Variables
  drilldown submenu, route-gated to /dashboard/:id which V2 serves
  through its own drilldown
- V2 cycle detection uses V2's own dependency helpers rather than
  casting its form model into V1 types

container/DashboardContainer/visualization is deliberately untouched:
it is the shared chart layer, not V1 code.
2026-08-21 17:30:25 +05:30
Abhi kumar
485aed0e1a feat(charts): support none/normal/percent stacking on TimeSeries and Bar (#12632)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- Charts take a `stack` prop (`none` | `normal` | `percent`) and hand it
to their config, which derives the fill bands, and for `percent` the
percentage y-axis and a 0–100 soft range. Callers stop computing bands
or transforming data — V1/V2 bar panels, Meter Explorer and Billing each
drop their `setBands` call and declare `stack` instead.
- Stacking is no longer bar-specific, so TimeSeries stacks too. The
upcoming area chart is built on TimeSeries and needs this.
- `percent` rescales each x-slice to its column total. Mixed-sign
columns divide by the signed total, so shares can fall outside 0–100 and
still sum to it; a column summing to zero yields zero. The percent range
is soft rather than hard so those out-of-band shares stay visible.
- Tooltips now report the pre-stack value, identically in every mode.
They used to recover it by subtracting the series below, which only
works while stacking is cumulative — `percent` discards the column
total, so the raw value cannot be derived from the plot's data at all.
- `stack` lives on the two chart prop types rather than the shared
config builder props, so the ~10 other consumers of that builder
(histogram, alert previews, infra metrics, …) never expose an option
they cannot honour.

No spec or API change: both bar panels still read the existing
`stackedBarChart` boolean and map it to `normal`/`none`. `percent` is
reachable from the chart layer but nothing selects it yet — that arrives
with the panel spec change.

#### Additional Information

- Behaviour outside dashboards should be unchanged, with one exception:
Meter Explorer and the V1 bar panel previously passed `seriesCount + 1`
when computing bands, emitting a trailing band pointing at a series that
does not exist (Billing passed the correct count). Deriving bands
centrally normalises all three.
- Thresholds still draw under `percent`, but no longer widen the scale —
they carry source-unit values, so one at 500ms would stretch a
percentage axis to 0–500.
- percent also swaps the unit to a percent formatter and sets *soft* 0–1
limits (they normalise to 0–1, we use 0–100). It applies those limits
only when the user set none; we always apply them, because our soft
limits come from `spec.axes` in the source unit and are meaningless once
values are normalised.
- Commits are split so each one builds and is reviewable on its own: the
stacking algorithm, the config derivation, the tooltip change, then the
chart/consumer migration.
2026-08-20 20:16:14 +00:00
683 changed files with 2664 additions and 46976 deletions

View File

@@ -20,16 +20,6 @@ You are the Playwright Test Generator for the SigNoz frontend. You take a plan w
await expect(page.getByRole('tab', { name: /alert rules/i })).toBeVisible();
});
```
- **Extended fixtures:** For features needing complex setup (seeded data, API calls, cleanup), import from domain-specific fixtures that extend `auth`. See [docs/contributing/tests/e2e.md](../../docs/contributing/tests/e2e.md) for the full pattern.
- `fixtures/alerts/alert-rules` — worker-scoped rule list + test-scoped rule factory
- `fixtures/alerts/alert-history` — extends alert-rules, adds history fixtures (waits on ruler evaluation)
```ts
// Alert list tests - need rules, no history
import { test, expect } from '../../../fixtures/alerts/alert-rules';
// Alert history tests - need evaluated history rows
import { test, expect } from '../../../fixtures/alerts/alert-history';
```
- **Test titles:** `TC-NN <short description>` — matches the planner's IDs.
- **Self-contained state.** The bootstrap creates a fresh stack with **zero** dashboards / alerts / etc. — never assume pre-existing data. Two cleanup shapes are valid; pick based on the spec size:
- **Per-test `try / finally`** — small specs (~ <10 scenarios) where each test owns its data.

View File

@@ -49,7 +49,6 @@ Don't try to start the stack yourself — it can take ~4 minutes on a cold build
- **The list pages render zero-state when the workspace is empty.** Many locators (search input, sort button, `new-dashboard-cta` testid, "All Dashboards" header) are absent in zero-state. A 30s timeout on those usually means the workspace was empty — seed first via `createDashboardViaApi`.
- **The "Enter dashboard name…" inline field is a `RequestDashboardBtn` (template-request feedback form), not a create flow.** Tests that try to use it to create a named dashboard will silently no-op. The only UI create paths are the "New dashboard" dropdown → "Create dashboard" (default name "Sample Title", see `DEFAULT_DASHBOARD_TITLE`) or "Import JSON".
- **Auth.** `tests/e2e/fixtures/auth.ts` logs in once per worker and caches `storageState` (cookies + localStorage with `AUTH_TOKEN`). For API-driven seeding/cleanup, use `authToken(page)` from `helpers/dashboards.ts` and pass `Authorization: Bearer <token>`. Never re-implement login.
- **Extended fixtures.** Domain-specific fixtures extend `auth` and add seeded data. Alerts uses `fixtures/alerts/alert-rules` (worker-scoped rule list, test-scoped factory) and `fixtures/alerts/alert-history` (extends alert-rules, waits on ruler evaluation). See [docs/contributing/tests/e2e.md](../../docs/contributing/tests/e2e.md) for the pattern. When a test fails on missing data, check if it imports the wrong fixture level.
- **Ant Design popovers** (sort menu, action menu) are click-toggle. The trigger element is often an inline `<svg>` with a `data-testid` — clicking it opens the popover; clicking it again closes. After selecting an option, the popover auto-closes. If a test interacts with the popover twice, wait for the menu items to be visible explicitly between toggles.
- **Artifacts.** Every failed test writes to `tests/e2e/artifacts/results/<test-slug>/` — the `error-context.md` accessibility snapshot is the fastest way to see what the page actually looked like when it failed.
- **Type-check.** After edits, run `npx tsc --noEmit -p tests/e2e/tsconfig.json` if it succeeds, or rely on `npx playwright test --list` to validate the spec parses.

21
.github/CODEOWNERS vendored
View File

@@ -152,32 +152,21 @@ go.mod @therealpandey
## Dashboard Types
/frontend/src/api/types/dashboard/ @SigNoz/pulse-frontend
/frontend/src/types/api/dashboard/ @SigNoz/pulse-frontend
/frontend/src/types/api/widgets/ @SigNoz/pulse-frontend
## Dashboard List
## Widget Card
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
# Dashboard Widget Page
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
## Dashboard Page
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
/frontend/src/container/WidgetCard/ @SigNoz/pulse-frontend
## Public Dashboard Page
/frontend/src/pages/PublicDashboard/ @SigNoz/pulse-frontend
/frontend/src/container/PublicDashboardContainer/ @SigNoz/pulse-frontend
## Dashboard Libs + Components
/frontend/src/lib/uPlotV2/ @SigNoz/pulse-frontend
/frontend/src/lib/visualization/ @SigNoz/pulse-frontend
/frontend/src/lib/dashboard/ @SigNoz/pulse-frontend
/frontend/src/lib/dashboardVariables/ @SigNoz/pulse-frontend
/frontend/src/components/NewSelect/ @SigNoz/pulse-frontend

View File

@@ -112,41 +112,6 @@ These two folders look similar but mean different things:
Rule of thumb: if it's a `test.extend` fixture, put it in `fixtures/`. If it's a function you call explicitly (or a constant the function uses), put it in `helpers/`. If it's a static file the helpers read, put it in `testdata/`.
### Extended fixtures
For features needing complex setup (API-seeded data, ruler evaluation waits, cleanup), create domain-specific fixtures that extend `auth`. Group them in `fixtures/<domain>/`.
**Fixture scopes:**
- **test scope** — fresh data per test. Use for mutations (edit, delete, rename).
- **worker scope** — shared across tests in one worker. Use for read-only data. Worker scope pays the setup cost once per worker instead of once per test.
**The alerts pattern** (`fixtures/alerts/`) demonstrates extending fixtures:
```
fixtures/alerts/
├── alert-rules.ts # extends auth — worker-scoped rule list + test-scoped factory
└── alert-history.ts # extends alert-rules — adds history fixtures (waits on ruler)
```
Specs import from the fixture they need:
```ts
// List tests — just need rules, no history
import { test, expect } from '../../../fixtures/alerts/alert-rules';
// History tests — need history rows from ruler evaluation
import { test, expect } from '../../../fixtures/alerts/alert-history';
```
**When creating new fixtures:**
1. **Identify scope** — Will tests mutate the data? If yes, test-scoped. If read-only, worker-scoped.
2. **Group by domain** — Put fixtures in `fixtures/<domain>/`. Helpers in `helpers/<domain>/`.
3. **Extend existing fixtures** — Chain from `auth` or another fixture to inherit its setup.
4. **Handle timeouts** — Worker-scoped fixtures that wait on backend processing need explicit timeouts.
5. **Clean up** — Always delete seeded data in the fixture teardown (after `use()`).
6. **Extract logic into functions** — Keep the `test.extend()` block lean; move setup/teardown logic to named functions so the extend block reads as a manifest of "what fixtures exist."
Each spec follows these principles:
1. **Directory per feature**: `tests/e2e/tests/<feature>/*.spec.ts`. Cross-resource junction concerns (e.g. cascade-delete) go in their own file, not packed into one giant spec.
@@ -267,14 +232,11 @@ cd tests/e2e
# Single feature dir
npx playwright test tests/alerts/ --project=chromium
# Single sub-area
npx playwright test tests/alerts/history/ --project=chromium
# Single file
npx playwright test tests/alerts/page.spec.ts --project=chromium
npx playwright test tests/alerts/alerts.spec.ts --project=chromium
# Single test by title grep
npx playwright test --project=chromium -g "AL-01"
npx playwright test --project=chromium -g "TC-01"
```
### Iterative modes
@@ -308,14 +270,7 @@ yarn test:staging
| `SIGNOZ_E2E_PASSWORD` | Admin password. Bootstrap writes the integration-test default. |
| `SIGNOZ_E2E_SEEDER_URL` | Seeder HTTP base URL — hit by specs that need per-test telemetry. |
Precedence in `playwright.config.ts`, lowest to highest: `.env` (user-provided, staging) `.env.local` (bootstrap-generated, local mode) → whatever is already in `process.env`. The config parses both files itself and only fills in keys the environment does not already define, so exporting a variable always wins:
```bash
# runs against a locally served frontend, not whatever .env.local points at
SIGNOZ_E2E_BASE_URL=http://127.0.0.1:3301 pnpm test tests/alerts
```
This is deliberately not `dotenv.config({ override: true })`. That flag makes the *file* beat `process.env`, which silently discarded exported values — including the `SIGNOZ_E2E_BASE_URL` in `pnpm test:staging`, whenever a `.env.local` happened to exist.
Loading order in `playwright.config.ts`: `.env` first (user-provided, staging), then `.env.local` with `override: true` (bootstrap-generated, local mode). Anything already set in `process.env` at yarn-test time wins because dotenv doesn't touch vars that are already present.
### Playwright options

View File

@@ -94,17 +94,12 @@ export const OnboardingV2 = Loadable(
export const DashboardsListPage = Loadable(
() =>
import(
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPage'
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPageV2'
),
);
export const DashboardPage = Loadable(
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPage'),
);
export const DashboardWidget = Loadable(
() =>
import(/* webpackChunkName: "DashboardWidgetPage" */ 'pages/DashboardWidget'),
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPageV2'),
);
export const DashboardPanelEditorPage = Loadable(

View File

@@ -13,7 +13,6 @@ import {
DashboardPage,
DashboardPanelEditorPage,
DashboardsListPage,
DashboardWidget,
EditRulesPage,
ErrorDetails,
ForgotPassword,
@@ -183,13 +182,6 @@ const routes: AppRoutes[] = [
isPrivate: false,
key: 'PUBLIC_DASHBOARD',
},
{
path: ROUTES.DASHBOARD_WIDGET,
exact: true,
component: DashboardWidget,
isPrivate: true,
key: 'DASHBOARD_WIDGET',
},
{
path: ROUTES.DASHBOARD_PANEL_EDITOR,
exact: true,

View File

@@ -53,7 +53,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
};
}
// anything else
console.error('ErrorResponseHandler: unclassified error');
console.error('any');
return {
statusCode: 500,
payload: null,

View File

@@ -1,36 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { CreatePublicDashboardProps } from 'types/api/dashboard/public/create';
/**
* @deprecated Use the generated `useCreatePublicDashboard` hook (or `createPublicDashboard` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const createPublicDashboard = async (
props: CreatePublicDashboardProps,
): Promise<SuccessResponseV2<CreatePublicDashboardProps>> => {
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props;
try {
const response = await axios.post(
`/dashboards/${dashboardId}/public`,
{ timeRangeEnabled, defaultTimeRange },
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default createPublicDashboard;

View File

@@ -1,27 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardDataProps, PayloadProps,PublicDashboardDataProps } from 'types/api/dashboard/public/get';
/**
* @deprecated Use the generated `useGetPublicDashboardData` hook (or `getPublicDashboardData` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const getPublicDashboardData = async (props: GetPublicDashboardDataProps): Promise<SuccessResponseV2<PublicDashboardDataProps>> => {
try {
const response = await axios.get<PayloadProps>(`/public/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getPublicDashboardData;

View File

@@ -1,27 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardMetaProps, PayloadProps,PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
/**
* @deprecated Use the generated `useGetPublicDashboard` hook (or `getPublicDashboard` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const getPublicDashboardMeta = async (props: GetPublicDashboardMetaProps): Promise<SuccessResponseV2<PublicDashboardMetaProps>> => {
try {
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}/public`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getPublicDashboardMeta;

View File

@@ -1,34 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { MetricRangePayloadV5 } from 'api/v5/v5';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardWidgetDataProps } from 'types/api/dashboard/public/getWidgetData';
/**
* @deprecated Use the generated `useGetPublicDashboardWidgetQueryRange` hook (or `getPublicDashboardWidgetQueryRange` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const getPublicDashboardWidgetData = async (props: GetPublicDashboardWidgetDataProps): Promise<SuccessResponseV2<MetricRangePayloadV5>> => {
try {
const response = await axios.get(`/public/dashboards/${props.id}/widgets/${props.index}/query_range`, {
params: {
startTime: props.startTime,
endTime: props.endTime,
},
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getPublicDashboardWidgetData;

View File

@@ -1,29 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps,RevokePublicDashboardAccessProps } from 'types/api/dashboard/public/delete';
/**
* @deprecated Use the generated `useDeletePublicDashboard` hook (or `deletePublicDashboard` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const revokePublicDashboardAccess = async (
props: RevokePublicDashboardAccessProps,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.delete<PayloadProps>(`/dashboards/${props.id}/public`);
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default revokePublicDashboardAccess;

View File

@@ -1,36 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { UpdatePublicDashboardProps } from 'types/api/dashboard/public/update';
/**
* @deprecated Use the generated `useUpdatePublicDashboard` hook (or `updatePublicDashboard` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const updatePublicDashboard = async (
props: UpdatePublicDashboardProps,
): Promise<SuccessResponseV2<UpdatePublicDashboardProps>> => {
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props;
try {
const response = await axios.put(
`/dashboards/${dashboardId}/public`,
{ timeRangeEnabled, defaultTimeRange },
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default updatePublicDashboard;

View File

@@ -1,23 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/create';
import { Dashboard } from 'types/api/dashboard/getAll';
const create = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.post<PayloadProps>('/dashboards', {
...props,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default create;

View File

@@ -1,19 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Dashboard, PayloadProps } from 'types/api/dashboard/getAll';
const getAll = async (): Promise<SuccessResponseV2<Dashboard[]>> => {
try {
const response = await axios.get<PayloadProps>('/dashboards');
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getAll;

View File

@@ -1,21 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/delete';
const deleteDashboard = async (
props: Props,
): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.delete<PayloadProps>(`/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default deleteDashboard;

View File

@@ -1,20 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/get';
import { Dashboard } from 'types/api/dashboard/getAll';
const get = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default get;

View File

@@ -1,23 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/lockUnlock';
const lock = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put<PayloadProps>(
`/dashboards/${props.id}/lock`,
{ lock: props.lock },
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default lock;

View File

@@ -1,23 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { PayloadProps, Props } from 'types/api/dashboard/update';
const update = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.put<PayloadProps>(`/dashboards/${props.id}`, {
...props.data,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default update;

View File

@@ -1,176 +0,0 @@
export default function ApacheIcon(): JSX.Element {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7112 1.05739C8.3796 1.30831 8.08524 1.60498 7.83691 1.93853L8.17977 2.58567C8.40218 2.26279 8.64677 1.95576 8.91177 1.66681C8.93063 1.64581 8.94091 1.63596 8.94091 1.63596L8.91177 1.66681C8.66003 1.95965 8.43081 2.27111 8.22606 2.59853C8.67305 2.56672 9.11808 2.51165 9.55934 2.43353C9.60936 2.25525 9.62374 2.06886 9.60168 1.88502C9.57962 1.70118 9.52154 1.52349 9.43077 1.3621C9.43077 1.3621 9.09991 0.828957 8.7112 1.05739Z"
fill="url(#paint0_linear_2061_4195)"
/>
<path
d="M7.55932 6.49365L7.42432 6.51722L7.49332 6.50651C7.51432 6.50265 7.53703 6.49837 7.55932 6.49365Z"
fill="#BE202E"
/>
<path
opacity="0.35"
d="M7.55932 6.49365L7.42432 6.51722L7.49332 6.50651C7.51432 6.50265 7.53703 6.49837 7.55932 6.49365Z"
fill="#BE202E"
/>
<path
d="M7.67407 5.92858L7.6955 5.92558C7.72464 5.9213 7.75336 5.91616 7.78122 5.91016L7.67493 5.92816L7.67407 5.92858Z"
fill="#BE202E"
/>
<path
opacity="0.35"
d="M7.67407 5.92858L7.6955 5.92558C7.72464 5.9213 7.75336 5.91616 7.78122 5.91016L7.67493 5.92816L7.67407 5.92858Z"
fill="#BE202E"
/>
<path
d="M7.16872 4.25736C7.273 4.0625 7.37858 3.87221 7.48543 3.6865C7.59629 3.49393 7.70829 3.3075 7.82143 3.12721L7.84115 3.09507C7.95315 2.91793 8.066 2.74779 8.17972 2.58464L7.83686 1.9375L7.75886 2.03393C7.65986 2.15736 7.55743 2.29107 7.452 2.43036C7.33329 2.58893 7.21115 2.75779 7.08729 2.93564C6.97286 3.09979 6.85672 3.27164 6.74058 3.44993C6.64158 3.60121 6.54258 3.75721 6.444 3.91707L6.43286 3.93507L6.879 4.8145C6.97443 4.62707 7.071 4.44136 7.16872 4.25736Z"
fill="url(#paint1_linear_2061_4195)"
/>
<path
d="M5.13606 9.22519C5.07692 9.38748 5.01763 9.55305 4.95821 9.72191L4.95563 9.72919L4.93035 9.80076C4.89049 9.91476 4.85535 10.015 4.77563 10.2503C4.92444 10.3467 5.0416 10.4847 5.11249 10.6472C5.10283 10.4581 5.01907 10.2804 4.87935 10.1526C5.16043 10.1981 5.44862 10.1654 5.71236 10.0581C5.97609 9.95074 6.20521 9.77291 6.37463 9.54405C6.40102 9.50088 6.42464 9.45607 6.44535 9.40991C6.37459 9.49741 6.28141 9.56408 6.17575 9.6028C6.07008 9.64152 5.95589 9.65084 5.84535 9.62976C6.20937 9.49539 6.51802 9.24316 6.72221 8.91319C6.76978 8.83691 6.81563 8.75376 6.86278 8.66162C6.6974 8.84328 6.48756 8.97872 6.25393 9.05463C6.02029 9.13053 5.77091 9.14426 5.53035 9.09448L5.16949 9.13391C5.15706 9.16434 5.14721 9.19476 5.13606 9.22519Z"
fill="url(#paint2_linear_2061_4195)"
/>
<path
d="M5.30448 8.417C5.38248 8.21528 5.46276 8.01157 5.54533 7.80586C5.62419 7.60871 5.70519 7.41057 5.78833 7.21143C5.87148 7.01228 5.95633 6.81228 6.0429 6.61143C6.1309 6.40828 6.22076 6.20557 6.31248 6.00328C6.40419 5.801 6.49633 5.60257 6.5889 5.408C6.62262 5.33714 6.65662 5.26643 6.6909 5.19586C6.75005 5.07414 6.80962 4.95343 6.86962 4.83371C6.87262 4.82728 6.87605 4.82086 6.87948 4.81443L6.4329 3.93457L6.41105 3.97014C6.30733 4.14157 6.20362 4.313 6.10205 4.49128C6.00048 4.66957 5.89848 4.853 5.80205 5.03814C5.7189 5.19443 5.63776 5.35157 5.55862 5.50957L5.51148 5.606C5.41419 5.80614 5.32633 5.99943 5.24705 6.18543C5.15705 6.396 5.07762 6.59686 5.00876 6.788C4.9629 6.91357 4.92305 7.03486 4.88362 7.151C4.85233 7.25043 4.82276 7.34986 4.79448 7.451C4.72762 7.68471 4.67048 7.91771 4.62305 8.15L5.07133 9.035C5.13076 8.87671 5.19162 8.71614 5.2539 8.55328L5.30448 8.417Z"
fill="url(#paint3_linear_2061_4195)"
/>
<path
d="M4.615 8.18082C4.55868 8.46014 4.51975 8.74268 4.49843 9.02682C4.49843 9.03668 4.49843 9.04653 4.49629 9.05639C4.3547 8.8778 4.1801 8.72808 3.982 8.61539C4.24526 8.95089 4.41819 9.34823 4.48429 9.76953C4.28982 9.78674 4.0942 9.75338 3.91643 9.67268C4.05057 9.80984 4.21714 9.91096 4.40072 9.96668C4.14974 10.0146 3.9168 10.1307 3.72743 10.3022C3.97375 10.178 4.25059 10.1271 4.525 10.1557C4.21858 11.024 3.91129 11.9827 3.604 13.0001C3.64664 12.988 3.6856 12.9655 3.71739 12.9346C3.74918 12.9037 3.7728 12.8654 3.78615 12.8231C3.841 12.6388 4.20443 11.4298 4.77443 9.84068L4.82372 9.70439L4.83743 9.66625C4.89772 9.49968 4.96015 9.32968 5.02472 9.15625L5.06758 9.03753V9.03539L4.62143 8.15039C4.61929 8.16025 4.61672 8.17053 4.615 8.18082Z"
fill="url(#paint4_linear_2061_4195)"
/>
<path
d="M6.94843 4.89059L6.90986 4.96987C6.87129 5.04959 6.83214 5.13102 6.79243 5.21416C6.74957 5.30416 6.70671 5.3963 6.66386 5.49059C6.64157 5.53816 6.621 5.58573 6.59743 5.63416C6.53057 5.7793 6.46286 5.92916 6.39429 6.08373C6.30857 6.27402 6.22286 6.47173 6.13714 6.67687C6.054 6.87259 5.96971 7.07516 5.88429 7.28459C5.80314 7.48459 5.721 7.68973 5.63786 7.90002C5.56386 8.08888 5.48914 8.28273 5.41371 8.48159C5.40986 8.49145 5.40643 8.50087 5.403 8.51073C5.32814 8.70873 5.25257 8.91187 5.17629 9.12016L5.17114 9.1343L5.532 9.09487L5.51057 9.09102C6.039 8.98351 6.52026 8.7126 6.88629 8.31659C7.0686 8.1182 7.22683 7.89896 7.35771 7.66345C7.47147 7.46033 7.57252 7.25036 7.66029 7.03473C7.74386 6.83245 7.824 6.61387 7.90114 6.37645C7.79448 6.43086 7.68084 6.47037 7.56343 6.49388C7.54157 6.49859 7.52057 6.50287 7.49657 6.50673C7.47257 6.51059 7.45071 6.51445 7.42757 6.51745C7.80325 6.36305 8.10458 6.06924 8.26843 5.69759C8.12155 5.79773 7.95733 5.86966 7.78414 5.90973C7.75586 5.91616 7.72757 5.92087 7.69843 5.92516L7.677 5.92816C7.80484 5.87656 7.92565 5.80903 8.03657 5.72716C8.05843 5.71087 8.07943 5.69373 8.1 5.67573C8.13129 5.64873 8.16086 5.62045 8.18914 5.59002C8.20714 5.57116 8.22471 5.55145 8.24186 5.53087C8.28293 5.48179 8.32059 5.42996 8.35457 5.37573C8.36529 5.35859 8.376 5.34145 8.38629 5.32345C8.39957 5.29773 8.41243 5.27245 8.42486 5.24716C8.481 5.13402 8.526 5.03288 8.56157 4.94459C8.57957 4.90173 8.595 4.85888 8.60829 4.82116C8.61386 4.80616 8.619 4.79116 8.62371 4.7783C8.63786 4.73545 8.64943 4.69816 8.65843 4.66473C8.66941 4.62595 8.67828 4.58661 8.685 4.54688C8.67015 4.5586 8.65454 4.56934 8.63829 4.57902C8.4822 4.66169 8.31419 4.71953 8.14029 4.75045H8.13257L8.08157 4.75859L8.09057 4.75473L6.957 4.87902L6.94843 4.89059Z"
fill="url(#paint5_linear_2061_4195)"
/>
<path
d="M8.22475 2.59871C8.12404 2.75343 8.01389 2.92914 7.89561 3.12843L7.87675 3.16014C7.77446 3.33157 7.66604 3.52114 7.55147 3.72886C7.45261 3.90771 7.34989 4.10014 7.24332 4.30614C7.15018 4.48586 7.05418 4.67671 6.95532 4.87871L8.08889 4.75443C8.33914 4.65567 8.55501 4.48583 8.70989 4.26586C8.74804 4.211 8.78618 4.15357 8.82432 4.09443C8.94089 3.91271 9.05489 3.71257 9.15689 3.51371C9.25018 3.33322 9.33429 3.14813 9.40889 2.95914C9.44758 2.86102 9.48092 2.76087 9.50875 2.65914C9.52932 2.58028 9.54561 2.50571 9.55804 2.43457C9.11675 2.51236 8.67172 2.56715 8.22475 2.59871Z"
fill="url(#paint6_linear_2061_4195)"
/>
<path
d="M7.49234 6.50635C7.46963 6.5102 7.44648 6.51406 7.42334 6.51706C7.44648 6.51406 7.47134 6.5102 7.49234 6.50635Z"
fill="#BE202E"
/>
<path
opacity="0.35"
d="M7.49234 6.50635C7.46963 6.5102 7.44648 6.51406 7.42334 6.51706C7.44648 6.51406 7.47134 6.5102 7.49234 6.50635Z"
fill="#BE202E"
/>
<path
d="M7.49234 6.50635C7.46963 6.5102 7.44648 6.51406 7.42334 6.51706C7.44648 6.51406 7.47134 6.5102 7.49234 6.50635Z"
fill="url(#paint7_linear_2061_4195)"
/>
<defs>
<linearGradient
id="paint0_linear_2061_4195"
x1="7.26579"
y1="1.16584"
x2="9.7782"
y2="0.46843"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F69923" />
<stop offset="0.312" stopColor="#F79A23" />
<stop offset="0.838" stopColor="#E97826" />
</linearGradient>
<linearGradient
id="paint1_linear_2061_4195"
x1="1.76109"
y1="12.4382"
x2="6.87606"
y2="1.48267"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.323" stopColor="#9E2064" />
<stop offset="0.63" stopColor="#C92037" />
<stop offset="0.751" stopColor="#CD2335" />
<stop offset="1" stopColor="#E97826" />
</linearGradient>
<linearGradient
id="paint2_linear_2061_4195"
x1="3.47784"
y1="11.6287"
x2="6.5258"
y2="5.10046"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#282662" />
<stop offset="0.095" stopColor="#662E8D" />
<stop offset="0.788" stopColor="#9F2064" />
<stop offset="0.949" stopColor="#CD2032" />
</linearGradient>
<linearGradient
id="paint3_linear_2061_4195"
x1="1.94596"
y1="11.7748"
x2="7.06094"
y2="0.819304"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.323" stopColor="#9E2064" />
<stop offset="0.63" stopColor="#C92037" />
<stop offset="0.751" stopColor="#CD2335" />
<stop offset="1" stopColor="#E97826" />
</linearGradient>
<linearGradient
id="paint4_linear_2061_4195"
x1="2.46768"
y1="11.0455"
x2="5.15578"
y2="5.28798"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#282662" />
<stop offset="0.095" stopColor="#662E8D" />
<stop offset="0.788" stopColor="#9F2064" />
<stop offset="0.949" stopColor="#CD2032" />
</linearGradient>
<linearGradient
id="paint5_linear_2061_4195"
x1="3.08048"
y1="12.3044"
x2="8.19546"
y2="1.3489"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.323" stopColor="#9E2064" />
<stop offset="0.63" stopColor="#C92037" />
<stop offset="0.751" stopColor="#CD2335" />
<stop offset="1" stopColor="#E97826" />
</linearGradient>
<linearGradient
id="paint6_linear_2061_4195"
x1="2.70679"
y1="12.9581"
x2="7.82195"
y2="2.00223"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.323" stopColor="#9E2064" />
<stop offset="0.63" stopColor="#C92037" />
<stop offset="0.751" stopColor="#CD2335" />
<stop offset="1" stopColor="#E97826" />
</linearGradient>
<linearGradient
id="paint7_linear_2061_4195"
x1="3.41759"
y1="12.4619"
x2="8.53277"
y2="1.50629"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.323" stopColor="#9E2064" />
<stop offset="0.63" stopColor="#C92037" />
<stop offset="0.751" stopColor="#CD2335" />
<stop offset="1" stopColor="#E97826" />
</linearGradient>
</defs>
</svg>
);
}

View File

@@ -1,28 +0,0 @@
export default function DockerIcon(): JSX.Element {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_2061_4220)">
<path
d="M13.574 5.82107C13.2652 5.60679 12.5575 5.52644 12.0042 5.63358C11.9398 5.09788 11.6439 4.62915 11.1292 4.21399L10.8332 3.99971L10.6274 4.30774C10.37 4.70951 10.2413 5.27198 10.2799 5.80768C10.2928 5.99517 10.3571 6.32998 10.5502 6.62461C10.37 6.73175 9.99686 6.86567 9.50789 6.86567H0.204685L0.17895 6.97281C0.0888774 7.5085 0.0888774 9.18254 1.14401 10.4682C1.9418 11.4458 3.12561 11.9414 4.68258 11.9414C8.05386 11.9414 10.5502 10.3209 11.7211 7.38797C12.1843 7.40136 13.1751 7.38797 13.677 6.38354C13.6898 6.35676 13.7156 6.30319 13.8056 6.10231L13.8571 5.99517L13.574 5.82107ZM7.6421 2.04443H6.22668V3.38367H7.6421V2.04443ZM7.6421 3.65151H6.22668V4.99074H7.6421V3.65151ZM5.96933 3.65151H4.5539V4.99074H5.96933V3.65151ZM4.29655 3.65151H2.88113V4.99074H4.29655V3.65151ZM2.62378 5.25859H1.20835V6.59782H2.62378V5.25859ZM4.29655 5.25859H2.88113V6.59782H4.29655V5.25859ZM5.96933 5.25859H4.5539V6.59782H5.96933V5.25859ZM7.6421 5.25859H6.22668V6.59782H7.6421V5.25859ZM9.31488 5.25859H7.89945V6.59782H9.31488V5.25859Z"
fill="#2396ED"
/>
</g>
<defs>
<clipPath id="clip0_2061_4220">
<rect
width="13.7143"
height="13.7143"
fill="white"
transform="translate(0.142822 0.143066)"
/>
</clipPath>
</defs>
</svg>
);
}

View File

@@ -1,36 +0,0 @@
export default function ElasticSearchIcon(): JSX.Element {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.38041 6.94508L9.70241 8.45858L13.0524 5.52158C13.1019 5.27892 13.1255 5.03171 13.1229 4.78408C13.1236 3.9846 12.8681 3.20592 12.3941 2.56216C11.92 1.9184 11.2522 1.44339 10.4885 1.20675C9.72487 0.970101 8.9055 0.984259 8.15047 1.24714C7.39544 1.51003 6.74446 2.00782 6.29291 2.66758L5.73291 5.56008L6.38041 6.94508Z"
fill="#FED10A"
/>
<path
d="M2.943 10.4593C2.89356 10.7062 2.86994 10.9575 2.8725 11.2093C2.87361 12.012 3.13177 12.7932 3.60915 13.4385C4.08654 14.0838 4.75804 14.5592 5.52528 14.7952C6.29252 15.0311 7.11514 15.0151 7.87262 14.7495C8.63009 14.4839 9.28259 13.9827 9.7345 13.3193L10.2845 10.4398L9.55 9.02928L6.215 7.50928L2.943 10.4593Z"
fill="#24BBB1"
/>
<path
d="M2.92394 4.71302L5.19994 5.25002L5.69994 2.66552C5.39077 2.43015 5.01365 2.30134 4.62509 2.29839C4.23654 2.29544 3.8575 2.41852 3.54479 2.64916C3.23208 2.8798 3.00256 3.20559 2.89063 3.57768C2.77869 3.94978 2.79039 4.34813 2.92394 4.71302Z"
fill="#EF5098"
/>
<path
d="M2.72503 5.2583C2.23266 5.42016 1.80253 5.73058 1.49379 6.14687C1.18505 6.56317 1.01286 7.0649 1.00091 7.58305C0.988962 8.1012 1.13784 8.61033 1.42706 9.04041C1.71628 9.4705 2.13164 9.80042 2.61603 9.9848L5.81003 7.0998L5.22653 5.8498L2.72503 5.2583Z"
fill="#17A8E0"
/>
<path
d="M10.312 13.3197C10.6209 13.5543 10.9975 13.6825 11.3853 13.6851C11.7732 13.6877 12.1514 13.5646 12.4634 13.3342C12.7755 13.1038 13.0044 12.7785 13.116 12.407C13.2276 12.0356 13.2159 11.6379 13.0825 11.2737L10.812 10.7412L10.312 13.3197Z"
fill="#93C83E"
/>
<path
d="M10.7735 10.145L13.2735 10.7285C13.7666 10.5672 14.1976 10.257 14.507 9.84058C14.8165 9.42415 14.9891 8.922 15.0013 8.40333C15.0134 7.88467 14.8643 7.375 14.5747 6.94457C14.2851 6.51414 13.8691 6.18413 13.384 6L10.1135 8.8665L10.7735 10.145Z"
fill="#0779A1"
/>
</svg>
);
}

View File

@@ -1,27 +0,0 @@
function HerokuIcon(): JSX.Element {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_2061_4245)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.6285 0H2.10787C1.31283 0 0.666748 0.644312 0.666748 1.44112V14.5602C0.666748 15.3557 1.31283 16 2.10787 16H13.6285C14.4237 16 15.0678 15.3557 15.0678 14.5602V1.44112C15.0678 0.644312 14.4237 0 13.6285 0ZM14.2677 14.5602C14.2677 14.9135 13.9815 15.1994 13.6285 15.1994H2.10787C1.75506 15.1994 1.46688 14.9135 1.46688 14.5602V1.44112C1.46688 1.08654 1.75506 0.800133 2.10787 0.800133H13.6285C13.9815 0.800133 14.2677 1.08654 14.2677 1.44112V14.5602ZM4.26699 13.6009L6.06823 12.0002L4.26699 10.3999V13.6009ZM10.7719 7.11485C10.4481 6.78927 9.8569 6.40038 8.86819 6.40038C7.78342 6.40038 6.66611 6.68302 5.86752 6.94177V2.40082H4.26704V9.33907L5.39807 8.82667C5.41688 8.81826 7.24025 8.00086 8.86819 8.00086C9.68027 8.00086 9.86022 8.44796 9.86885 8.82158V13.6009H11.4676V8.801C11.4693 8.6983 11.4592 7.81051 10.7719 7.11485ZM8.66761 5.00027H10.2681C10.9912 4.1811 11.3597 3.30903 11.4677 2.40066H9.86881C9.69063 3.30726 9.29643 4.17424 8.66761 5.00027Z"
fill="#6762A6"
/>
</g>
<defs>
<clipPath id="clip0_2061_4245">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export default HerokuIcon;

File diff suppressed because one or more lines are too long

View File

@@ -1,68 +0,0 @@
export default function MongoDBIcon(): JSX.Element {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.26568 13.0004L6.94382 12.8937C6.94382 12.8937 6.98668 11.2651 6.39739 11.1507C6.01168 10.7015 6.45439 -8.02403 7.86439 11.0868C7.59687 11.2225 7.39217 11.4564 7.29311 11.7395C7.24001 12.1577 7.23082 12.5803 7.26568 13.0004Z"
fill="url(#paint0_linear_2061_4238)"
/>
<path
d="M7.43957 11.4272C8.29654 10.7821 8.95282 9.90701 9.33214 8.90369C9.71147 7.90037 9.79826 6.81 9.58243 5.75931C8.95243 2.98002 7.46058 2.06631 7.29986 1.71745C7.1612 1.50022 7.04284 1.27068 6.94629 1.03174L7.065 8.7756C7.065 8.7756 6.819 11.1422 7.43957 11.4272Z"
fill="url(#paint1_linear_2061_4238)"
/>
<path
d="M6.78015 11.5296C6.78015 11.5296 4.15687 9.74286 4.30858 6.58214C4.32273 5.62928 4.54121 4.69054 4.94926 3.82935C5.3573 2.96816 5.94542 2.20456 6.67387 1.59014C6.759 1.51782 6.82663 1.42715 6.87168 1.32493C6.91674 1.22272 6.93805 1.11163 6.93401 1C7.0973 1.35143 7.07072 6.247 7.08787 6.81957C7.1543 9.04686 6.96401 11.1091 6.78015 11.5296Z"
fill="url(#paint2_linear_2061_4238)"
/>
<defs>
<linearGradient
id="paint0_linear_2061_4238"
x1="5.1021"
y1="7.10853"
x2="8.80138"
y2="8.36386"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.231" stopColor="#999875" />
<stop offset="0.563" stopColor="#9B9977" />
<stop offset="0.683" stopColor="#A09F7E" />
<stop offset="0.768" stopColor="#A9A889" />
<stop offset="0.837" stopColor="#B7B69A" />
<stop offset="0.896" stopColor="#C9C7B0" />
<stop offset="0.948" stopColor="#DEDDCB" />
<stop offset="0.994" stopColor="#F8F6EB" />
<stop offset="1" stopColor="#FBF9EF" />
</linearGradient>
<linearGradient
id="paint1_linear_2061_4238"
x1="6.45855"
y1="0.976404"
x2="8.09399"
y2="11.1888"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#48A547" />
<stop offset="1" stopColor="#3F9143" />
</linearGradient>
<linearGradient
id="paint2_linear_2061_4238"
x1="4.08293"
y1="6.89501"
x2="8.47176"
y2="5.42519"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41A247" />
<stop offset="0.352" stopColor="#4BA74B" />
<stop offset="0.956" stopColor="#67B554" />
<stop offset="1" stopColor="#69B655" />
</linearGradient>
</defs>
</svg>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -1,22 +0,0 @@
function NginxIcon(): JSX.Element {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.97775 1H7.00561C7.14837 1.068 7.28743 1.14353 7.42218 1.22629C8.97332 2.11829 10.5245 3.01057 12.0756 3.90314C12.1356 3.93517 12.1846 3.9845 12.2163 4.04472C12.2479 4.10493 12.2607 4.17327 12.253 4.24086C12.2496 6.12186 12.253 8.00243 12.2509 9.88257C12.2307 9.9723 12.1759 10.0504 12.0983 10.0999C10.4489 11.0496 8.79932 11.9987 7.14961 12.9473C7.10963 12.9778 7.06144 12.9956 7.01125 12.9984C6.96106 13.0012 6.91118 12.9889 6.86803 12.9631C5.21661 12.0163 3.56675 11.0677 1.91846 10.1174C1.86489 10.0921 1.82003 10.0515 1.78952 10.0007C1.75901 9.94988 1.74423 9.89119 1.74703 9.832C1.74703 7.95143 1.74703 6.071 1.74703 4.19071C1.74299 4.13178 1.75661 4.07299 1.78616 4.02184C1.8157 3.97069 1.85983 3.92951 1.91289 3.90357C3.46203 3.01271 5.01118 2.12129 6.56032 1.22929C6.69832 1.15043 6.83375 1.06686 6.97775 1Z"
fill="#019639"
/>
<path
d="M3.90007 4.65924C3.90007 6.21039 3.90007 7.76167 3.90007 9.3131C3.89809 9.39901 3.91326 9.48446 3.94468 9.56445C3.9761 9.64444 4.02315 9.71736 4.08307 9.77896C4.19794 9.89243 4.34824 9.96309 4.5089 9.97916C4.66956 9.99522 4.83087 9.95572 4.96593 9.86724C5.05634 9.80581 5.13035 9.72321 5.18152 9.62662C5.23269 9.53003 5.25946 9.4224 5.2595 9.3131C5.2595 8.19024 5.25736 7.06739 5.2595 5.94453C6.28322 7.17024 7.30907 8.39424 8.33707 9.61653C8.4799 9.76122 8.65676 9.86772 8.85145 9.92628C9.04614 9.98484 9.25242 9.99357 9.45136 9.95167C9.59184 9.924 9.71973 9.85198 9.81624 9.74622C9.91275 9.64045 9.97278 9.50652 9.9875 9.3641C9.98979 7.78096 9.98979 6.19796 9.9875 4.6151C9.97275 4.44614 9.8952 4.28884 9.77016 4.17425C9.64512 4.05966 9.48168 3.99609 9.31207 3.99609C9.14247 3.99609 8.97902 4.05966 8.85399 4.17425C8.72895 4.28884 8.6514 4.44614 8.63665 4.6151C8.63665 5.75596 8.62979 6.89553 8.63665 8.03596C7.63122 6.85053 6.63822 5.65481 5.63665 4.4651C5.5046 4.29578 5.32979 4.16474 5.13023 4.08549C4.93067 4.00624 4.71359 3.98165 4.50136 4.01424C4.34059 4.03214 4.19154 4.10704 4.08124 4.22536C3.97093 4.34369 3.90666 4.49761 3.90007 4.65924Z"
fill="white"
/>
</svg>
);
}
export default NginxIcon;

File diff suppressed because one or more lines are too long

View File

@@ -1,60 +0,0 @@
export default function RedisIcon(): JSX.Element {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_2061_4282)">
<path
d="M13.3198 10.158C12.5879 10.5395 8.79654 12.0984 7.98938 12.5192C7.18221 12.94 6.73382 12.936 6.09616 12.6312C5.45855 12.3263 1.42388 10.6966 0.697072 10.3492C0.333858 10.1756 0.142822 10.029 0.142822 9.8906V8.50438C0.142822 8.50438 5.3955 7.3609 6.24348 7.05667C7.09141 6.75244 7.38563 6.74145 8.10723 7.00578C8.82895 7.2702 13.1439 8.0487 13.8571 8.30992L13.8568 9.67653C13.8569 9.81356 13.6923 9.96388 13.3198 10.158Z"
fill="#912626"
/>
<path
d="M13.3195 8.77972C12.5877 9.16104 8.79642 10.72 7.98926 11.1407C7.18215 11.5616 6.73376 11.5575 6.09615 11.2527C5.45849 10.9481 1.42397 9.31805 0.697221 8.97086C-0.029529 8.62345 -0.0447433 8.38436 0.66915 8.10482C1.38304 7.82518 5.39544 6.25098 6.24353 5.94675C7.09145 5.64263 7.38561 5.63154 8.10722 5.89597C8.82888 6.16029 12.5975 7.66034 13.3106 7.9215C14.024 8.18298 14.0513 8.39829 13.3195 8.77972Z"
fill="#C6302B"
/>
<path
d="M13.3198 7.91483C12.5879 8.29637 8.79654 9.85519 7.98938 10.2762C7.18221 10.6968 6.73382 10.6928 6.09616 10.388C5.4585 10.0833 1.42388 8.45338 0.697072 8.10597C0.333858 7.9324 0.142822 7.78604 0.142822 7.64756V6.26119C0.142822 6.26119 5.3955 5.11776 6.24348 4.81353C7.09141 4.50929 7.38563 4.49826 8.10723 4.76263C8.829 5.02701 13.144 5.80535 13.8571 6.06661L13.8568 7.43338C13.8569 7.57036 13.6923 7.72069 13.3198 7.91483Z"
fill="#912626"
/>
<path
d="M13.3195 6.53701C12.5877 6.91844 8.79642 8.47726 7.98926 8.89817C7.18215 9.31892 6.73376 9.3148 6.09615 9.00998C5.45849 8.70537 1.42397 7.07541 0.697221 6.72816C-0.029529 6.38085 -0.0447433 6.14171 0.66915 5.86207C1.38304 5.58258 5.39549 4.00828 6.24353 3.7041C7.09145 3.39992 7.38561 3.38889 8.10722 3.65326C8.82888 3.91758 12.5975 5.41753 13.3106 5.6788C14.024 5.94023 14.0513 6.15558 13.3195 6.53701Z"
fill="#C6302B"
/>
<path
d="M13.3198 5.58855C12.5879 5.96998 8.79654 7.5289 7.98938 7.94987C7.18221 8.37062 6.73382 8.36649 6.09616 8.06167C5.4585 7.75701 1.42388 6.12705 0.697072 5.7798C0.333858 5.60607 0.142822 5.45965 0.142822 5.32133V3.9349C0.142822 3.9349 5.3955 2.79153 6.24348 2.48735C7.09141 2.18307 7.38563 2.17214 8.10723 2.43646C8.829 2.70083 13.144 3.47917 13.8571 3.74044L13.8568 5.10715C13.8569 5.24403 13.6923 5.39435 13.3198 5.58855Z"
fill="#912626"
/>
<path
d="M13.3195 4.21078C12.5876 4.59221 8.79639 6.15113 7.98923 6.57188C7.18212 6.99263 6.73373 6.98851 6.09612 6.68385C5.45852 6.37903 1.42394 4.74917 0.697248 4.40187C-0.0295558 4.05462 -0.0447165 3.81542 0.669123 3.53583C1.38302 3.2563 5.39546 1.68221 6.2435 1.37792C7.09143 1.07369 7.38559 1.06276 8.1072 1.32714C8.82886 1.59151 12.5975 3.09146 13.3106 3.35272C14.0239 3.61394 14.0513 3.82935 13.3195 4.21078Z"
fill="#C6302B"
/>
<path
d="M8.67572 2.86218L7.49661 2.9846L7.23267 3.61974L6.80635 2.91099L5.44483 2.78863L6.46076 2.42226L6.15594 1.85986L7.1071 2.23186L8.00378 1.93829L7.76142 2.51981L8.67572 2.86218ZM7.16228 5.94351L4.96172 5.03081L8.11494 4.54679L7.16228 5.94351ZM4.11138 3.21522C5.04219 3.21522 5.79674 3.50772 5.79674 3.86847C5.79674 4.22933 5.04219 4.52177 4.11138 4.52177C3.18058 4.52177 2.42603 4.22927 2.42603 3.86847C2.42603 3.50772 3.18058 3.21522 4.11138 3.21522Z"
fill="white"
/>
<path
d="M10.0693 3.0357L11.9355 3.77316L10.0709 4.50993L10.0693 3.0357Z"
fill="#621B1C"
/>
<path
d="M8.00464 3.85234L10.0693 3.03564L10.0709 4.50988L9.86844 4.58906L8.00464 3.85234Z"
fill="#9A2928"
/>
</g>
<defs>
<clipPath id="clip0_2061_4282">
<rect
width="13.7143"
height="13.7143"
fill="white"
transform="translate(0.142822 0.143066)"
/>
</clipPath>
</defs>
</svg>
);
}

View File

@@ -8,14 +8,12 @@ export interface AlertBreadcrumbProps {
items: BreadcrumbItemConfig[];
className?: string;
showDivider?: boolean;
testId?: string;
}
function AlertBreadcrumb({
items,
className,
showDivider = true,
testId,
}: AlertBreadcrumbProps): JSX.Element {
const breadcrumbItems = items.map((item) => ({
title: <BreadcrumbItem {...item} />,
@@ -26,7 +24,6 @@ function AlertBreadcrumb({
<Breadcrumb
className={`${styles.breadcrumb} ${className || ''}`}
items={breadcrumbItems}
data-testid={testId}
/>
{showDivider && <Divider className={styles.divider} />}
</>

View File

@@ -8,7 +8,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { X } from '@signozhq/icons';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataSource } from 'types/common/queryBuilder';

View File

@@ -5,16 +5,16 @@ import { useHistory, useLocation } from 'react-router-dom';
import { Color } from '@signozhq/design-tokens';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { ViewMenuAction } from 'container/WidgetCard/config';
import GridCard from 'container/WidgetCard/Card';
import { Card } from 'container/WidgetCard/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';

View File

@@ -5,15 +5,15 @@ import { useHistory, useLocation } from 'react-router-dom';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { ViewMenuAction } from 'container/WidgetCard/config';
import GridCard from 'container/WidgetCard/Card';
import { Card } from 'container/WidgetCard/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { UpdateTimeInterval } from 'store/actions';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import { Card } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { CardContainer } from 'container/GridCardLayout/styles';
import { CardContainer } from 'container/WidgetCard/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronUp } from '@signozhq/icons';
import { AppState } from 'store/reducers';

View File

@@ -1,7 +1,7 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 as uuidv4 } from 'uuid';

View File

@@ -6,9 +6,9 @@ import { Col, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { ViewMenuAction } from 'container/WidgetCard/config';
import GridCard from 'container/WidgetCard/Card';
import { Card } from 'container/WidgetCard/styles';
import { Button } from 'container/MetricsApplication/Tabs/styles';
import { useGraphClickHandler } from 'container/MetricsApplication/Tabs/util';
import { useIsDarkMode } from 'hooks/useDarkMode';

View File

@@ -8,7 +8,7 @@ import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getQueryPayloadFromWidgetsData } from 'pages/Celery/CeleryOverview/CeleryOverviewUtils';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';

View File

@@ -1,7 +1,7 @@
import { QueryParams } from 'constants/query';
import { History, Location } from 'history';
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuidv4 } from 'uuid';

View File

@@ -4,10 +4,9 @@ import { useSelector } from 'react-redux';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
import useUpdatedQuery from 'container/WidgetCard/hooks/useResolveQuery';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useNotifications } from 'hooks/useNotifications';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { AppState } from 'store/reducers';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
@@ -80,7 +79,6 @@ export function useNavigateToExplorer(): (
);
const { getUpdatedQuery } = useUpdatedQuery();
const { dashboardData } = useDashboardStore();
const { notifications } = useNotifications();
return useCallback(
@@ -112,7 +110,6 @@ export function useNavigateToExplorer(): (
panelTypes: PANEL_TYPES.TIME_SERIES,
timePreferance: 'GLOBAL_TIME',
},
dashboardData,
})
.then((query) => {
preparedQuery = query;
@@ -136,13 +133,6 @@ export function useNavigateToExplorer(): (
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
},
[
prepareQuery,
minTime,
maxTime,
getUpdatedQuery,
dashboardData,
notifications,
],
[prepareQuery, minTime, maxTime, getUpdatedQuery, notifications],
);
}

View File

@@ -28,7 +28,7 @@ import {
} from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
import { generateGridTitle } from 'utils/generateGridTitle';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import isEqual from 'lodash-es/isEqual';

View File

@@ -1,585 +0,0 @@
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
import { VirtuosoMockContext } from 'react-virtuoso';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import configureStore from 'redux-mock-store';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import VariableItem from '../../../container/DashboardContainer/DashboardVariablesSelection/VariableItem';
// Mock the dashboard variables query
jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
__esModule: true,
default: jest.fn(() =>
Promise.resolve({
payload: {
variableValues: ['option1', 'option2', 'option3', 'option4'],
},
}),
),
}));
// Mock scrollIntoView which isn't available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Constants
const TEST_VARIABLE_NAME = 'test_variable';
const TEST_VARIABLE_ID = 'test-var-id';
// Create a mock store
const mockStore = configureStore([])({
globalTime: {
minTime: Date.now() - 3600000, // 1 hour ago
maxTime: Date.now(),
},
});
// Test data
const createMockVariable = (
overrides: Partial<IDashboardVariable> = {},
): IDashboardVariable => ({
id: TEST_VARIABLE_ID,
name: TEST_VARIABLE_NAME,
description: 'Test variable description',
type: 'QUERY',
queryValue: 'SELECT DISTINCT value FROM table',
customValue: '',
sort: 'ASC',
multiSelect: false,
showALLOption: true,
selectedValue: [],
allSelected: false,
...overrides,
});
function TestWrapper({ children }: { children: React.ReactNode }): JSX.Element {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return (
<Provider store={mockStore}>
<QueryClientProvider client={queryClient}>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 40 }}
>
{children}
</VirtuosoMockContext.Provider>
</QueryClientProvider>
</Provider>
);
}
describe('VariableItem Integration Tests', () => {
let user: ReturnType<typeof userEvent.setup>;
let mockOnValueUpdate: jest.Mock;
beforeEach(() => {
user = userEvent.setup();
mockOnValueUpdate = jest.fn();
jest.clearAllMocks();
});
// ===== 1. INTEGRATION WITH CUSTOMSELECT =====
describe('CustomSelect Integration (VI)', () => {
it('VI-01: Single select variable integration', async () => {
const variable = createMockVariable({
multiSelect: false,
type: 'CUSTOM',
customValue: 'option1,option2,option3',
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
// Should render with CustomSelect
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
await user.click(combobox);
await waitFor(() => {
expect(screen.getByText('option1')).toBeInTheDocument();
expect(screen.getByText('option2')).toBeInTheDocument();
expect(screen.getByText('option3')).toBeInTheDocument();
});
// Select an option
const option1 = screen.getByText('option1');
await user.click(option1);
expect(mockOnValueUpdate).toHaveBeenCalledWith(
TEST_VARIABLE_NAME,
TEST_VARIABLE_ID,
'option1',
false,
);
});
});
// ===== 2. INTEGRATION WITH CUSTOMMULTISELECT =====
describe('CustomMultiSelect Integration (VI)', () => {
it('VI-02: Multi select variable integration', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
customValue: 'option1,option2,option3,option4',
showALLOption: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
// Should render with CustomMultiSelect
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
await user.click(combobox);
await waitFor(() => {
// Should show ALL option
expect(screen.getByText('ALL')).toBeInTheDocument();
});
// Wait for Virtuoso to render the custom options
await waitFor(
() => {
expect(screen.getByText('option1')).toBeInTheDocument();
expect(screen.getByText('option2')).toBeInTheDocument();
expect(screen.getByText('option3')).toBeInTheDocument();
expect(screen.getByText('option4')).toBeInTheDocument();
},
{ timeout: 5000 },
);
});
});
// ===== 3. TEXTBOX VARIABLE TYPE =====
describe('Textbox Variable Integration', () => {
it('VI-03: Textbox variable handling', async () => {
const variable = createMockVariable({
type: 'TEXTBOX',
selectedValue: 'initial-value',
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
// Should render a regular input
const textInput = screen.getByDisplayValue('initial-value');
expect(textInput).toBeInTheDocument();
expect(textInput.tagName).toBe('INPUT');
// Clear and type new value
await user.clear(textInput);
await user.type(textInput, 'new-text-value');
// Blur the input to trigger the value update
await user.tab();
// Should call onValueUpdate after blur
await waitFor(
() => {
expect(mockOnValueUpdate).toHaveBeenCalledWith(
TEST_VARIABLE_NAME,
TEST_VARIABLE_ID,
'new-text-value',
false,
);
},
{ timeout: 1000 },
);
});
});
// ===== 4. VALUE PERSISTENCE AND STATE MANAGEMENT =====
describe('Value Persistence and State Management', () => {
it('VI-04: All selected state handling', () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
customValue: 'service1,service2,service3',
selectedValue: ['service1', 'service2', 'service3'],
allSelected: true,
showALLOption: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
// Should show "ALL" instead of individual values
expect(screen.getByText('ALL')).toBeInTheDocument();
});
it('VI-05: Dropdown behavior with temporary selections', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
customValue: 'item1,item2,item3',
selectedValue: ['item1'],
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
const combobox = screen.getByRole('combobox');
await user.click(combobox);
// Wait for dropdown to open
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toBeInTheDocument();
});
// Verify the component renders without crashing
expect(combobox).toBeInTheDocument();
});
});
// ===== 6. ACCESSIBILITY AND USER EXPERIENCE =====
describe('Accessibility and User Experience', () => {
it('VI-06: Variable description tooltip', async () => {
const variable = createMockVariable({
description: 'This variable controls the service selection',
type: 'CUSTOM',
customValue: 'service1,service2',
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
// Should show info icon
const infoIcon = document.querySelector('.info-icon');
expect(infoIcon).toBeInTheDocument();
// Hover to show tooltip
if (infoIcon) {
await user.hover(infoIcon);
}
await waitFor(() => {
expect(
screen.getByText('This variable controls the service selection'),
).toBeInTheDocument();
});
});
it('VI-07: Variable name display', () => {
const variable = createMockVariable({
name: 'service_name',
type: 'CUSTOM',
customValue: 'service1,service2',
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
// Should show variable name with $ prefix
expect(screen.getByText('$service_name')).toBeInTheDocument();
});
it('VI-08: Max tag count behavior', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
customValue: 'tag1,tag2,tag3,tag4,tag5,tag6,tag7',
selectedValue: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'],
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
// Wait for component to render
await waitFor(() => {
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
});
// Should show limited number of tags with "+ X more"
const tags = document.querySelectorAll('.ant-select-selection-item');
// The component should render without crashing
expect(tags.length).toBeGreaterThanOrEqual(0);
});
});
// ===== 8. SEARCH INTERACTION TESTS =====
describe('Search Interaction Tests', () => {
it('VI-14: Search persistence across dropdown open/close', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
multiSelect: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
const combobox = screen.getByRole('combobox');
await user.click(combobox);
await waitFor(() => {
expect(screen.getByText('ALL')).toBeInTheDocument();
});
const searchInput = document.querySelector(
'.ant-select-selection-search-input',
);
expect(searchInput).toBeInTheDocument();
if (searchInput) {
await user.type(searchInput, 'search-text');
}
// Verify search text is in input
await waitFor(() => {
expect(searchInput).toHaveValue('search-text');
});
// Press Escape to close dropdown
await user.keyboard('{Escape}');
// Dropdown should close and search text should be cleared
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
expect(searchInput).toHaveValue('');
});
});
});
// ===== 9. ADVANCED KEYBOARD NAVIGATION =====
describe('Advanced Keyboard Navigation (VI)', () => {
it('VI-15: Shift + Arrow + Del chip deletion in multiselect', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
multiSelect: true,
selectedValue: ['option1', 'option2', 'option3'],
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
const combobox = screen.getByRole('combobox');
await user.click(combobox);
// Navigate to chips using arrow keys
await user.keyboard('{ArrowLeft}');
// Use Shift + Arrow to navigate between chips
await user.keyboard('{Shift>}{ArrowLeft}{/Shift}');
// Use Del to delete the active chip
await user.keyboard('{Delete}');
// Note: The component may not immediately call onValueUpdate
// This test verifies the chip deletion behavior
await waitFor(() => {
// Check if a chip was removed from the selection
const selectionItems = document.querySelectorAll(
'.ant-select-selection-item',
);
expect(selectionItems.length).toBeLessThan(3);
});
});
});
// ===== 11. ADVANCED UI STATES =====
describe('Advanced UI States (VI)', () => {
it('VI-19: No data with previous value selected in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: '',
multiSelect: true,
selectedValue: ['previous-value'],
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
// Wait for component to initialize
await waitFor(() => {
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
});
const combobox = screen.getByRole('combobox');
await user.click(combobox);
// Should show no data message (the component may not show this exact text)
await waitFor(() => {
// Check if dropdown is empty or shows no data indication
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toBeInTheDocument();
});
// Verify the component renders without crashing
expect(combobox).toBeInTheDocument();
});
it('VI-20: Always editable accessibility in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2',
multiSelect: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
const combobox = screen.getByRole('combobox');
// Should be editable
expect(combobox).not.toBeDisabled();
await user.click(combobox);
expect(combobox).toHaveFocus();
// Should still be interactive
expect(combobox).not.toBeDisabled();
});
});
// ===== 13. DROPDOWN PERSISTENCE =====
describe('Dropdown Persistence (VI)', () => {
it('VI-24: Dropdown stays open for non-save actions in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
multiSelect: true,
});
render(
<TestWrapper>
<VariableItem
variableData={variable}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
/>
</TestWrapper>,
);
// Wait for component to initialize
await waitFor(() => {
const combobox = screen.getByRole('combobox');
expect(combobox).toBeInTheDocument();
});
const combobox = screen.getByRole('combobox');
await user.click(combobox);
await waitFor(() => {
expect(screen.getByText('ALL')).toBeInTheDocument();
});
// Navigate with arrow keys (non-save action)
await user.keyboard('{ArrowDown}');
await user.keyboard('{ArrowDown}');
// Dropdown should still be open
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toBeInTheDocument();
});
// Verify the component renders without crashing
expect(combobox).toBeInTheDocument();
// Only ESC should close the dropdown
await user.keyboard('{Escape}');
await waitFor(() => {
const dropdown = document.querySelector('.ant-select-dropdown');
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
});
});
});
});

View File

@@ -1,11 +1,26 @@
import { uniqueOptions } from 'container/DashboardContainer/DashboardVariablesSelection/util';
import { OptionData } from './types';
export const SPACEKEY = ' ';
export const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value
/** De-duplicate options by `value`, keeping the first occurrence. */
export const uniqueOptions = (options: OptionData[]): OptionData[] => {
const deduped: OptionData[] = [];
const seenValues = new Set<string>();
options.forEach((option) => {
const value = option.value || '';
if (seenValues.has(value)) {
return;
}
seenValues.add(value);
deduped.push(option);
});
return deduped;
};
export const prioritizeOrAddOptionForSingleSelect = (
options: OptionData[],
value: string,

View File

@@ -2,7 +2,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { Spline } from '@signozhq/icons';
import { EQueryType } from 'types/common/dashboard';
import QueryTypeTag from '../QueryTypeTag';
import QueryTypeTag from 'components/QueryTypeTag/QueryTypeTag';
interface IPlotTagProps {
queryType: EQueryType;

View File

@@ -17,12 +17,6 @@ jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { dashboardData: undefined } => ({
dashboardData: undefined,
}),
}));
// Shrink the suggestion-fetch debounce (300ms in prod) so these integration
// tests aren't paced by it; coalescing semantics stay intact.
jest.mock('../QuerySearch/constants', () => ({

View File

@@ -21,12 +21,6 @@ jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { dashboardData: undefined } => ({
dashboardData: undefined,
}),
}));
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
const handleRunQuery = jest.fn();
return {
@@ -152,15 +146,16 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
/>,
);
// Wait for debounced API call (300ms debounce + some buffer)
await waitFor(() => expect(mockedGetKeysOnMount).toHaveBeenCalled(), {
timeout: 2000,
});
const lastArgs = mockedGetKeysOnMount.mock.calls[
mockedGetKeysOnMount.mock.calls.length - 1
]?.[0] as { signal: unknown; searchText: string };
expect(lastArgs).toMatchObject({ signal: DataSource.LOGS, searchText: '' });
// Wait for the mount fetch specifically. A debounced fetch from an earlier test
// can still land after mockClear(), so waiting on "any call" would let this
// assert against that one instead and make the result order-dependent.
await waitFor(
() =>
expect(mockedGetKeysOnMount).toHaveBeenCalledWith(
expect.objectContaining({ signal: DataSource.LOGS, searchText: '' }),
),
{ timeout: 2000 },
);
});
it('calls provided onRun on Mod-Enter', async () => {

View File

@@ -31,12 +31,6 @@ jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { dashboardData: undefined } => ({
dashboardData: undefined,
}),
}));
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
getKeySuggestions: jest.fn().mockResolvedValue({
data: { data: { keys: {} } },

View File

@@ -1,5 +1,5 @@
import { Typography } from '@signozhq/ui/typography';
import { timeItems } from 'container/NewWidget/RightContainer/timeItems';
import { timeItems } from 'constants/timePreference';
export const menuItems = timeItems.map((item) => ({
key: item.enum,

View File

@@ -6,7 +6,7 @@ import { Typography } from '@signozhq/ui/typography';
import TimeItems, {
timePreferance,
timePreferenceType,
} from 'container/NewWidget/RightContainer/timeItems';
} from 'constants/timePreference';
import { menuItems } from './config';

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { CircleAlert } from '@signozhq/icons';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { ThresholdProps } from 'types/api/widgets/threshold';
import { getBackgroundColorAndThresholdCheck } from './utils';

View File

@@ -1,5 +1,5 @@
import { evaluateThresholdWithConvertedValue } from 'container/GridTableComponent/utils';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { evaluateThresholdWithConvertedValue } from 'container/WidgetCard/TablePanel/utils';
import { ThresholdProps } from 'types/api/widgets/threshold';
function doesValueSatisfyThreshold(
rawValue: number,

View File

@@ -1,6 +1,6 @@
import Uplot from 'components/Uplot';
import GridTableComponent from 'container/GridTableComponent';
import GridValueComponent from 'container/GridValueComponent';
import GridTableComponent from 'container/WidgetCard/TablePanel';
import GridValueComponent from 'container/WidgetCard/ValuePanel';
import LogsPanelComponent from 'container/LogsPanelTable/LogsPanelComponent';
import TracesTableComponent from 'container/TracesTableComponent/TracesTableComponent';
import { DataSource } from 'types/common/queryBuilder';

View File

@@ -16,7 +16,6 @@ const ROUTES = {
APPLICATION: '/services',
ALL_DASHBOARD: '/dashboard',
DASHBOARD: '/dashboard/:dashboardId',
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
DASHBOARD_PANEL_EDITOR: '/dashboard/:dashboardId/panel/:panelId',
EDIT_ALERTS: '/alerts/edit',
LIST_ALL_ALERT: '/alerts',

View File

@@ -29,7 +29,6 @@ function PopoverContent({
<Link
to={`${ROUTES.LOGS_EXPLORER}?${relatedLogsLink}`}
className="contributor-row-popover-buttons__button"
data-testid="alert-popover-view-logs"
>
<div className="icon">
<LogsIcon />
@@ -41,7 +40,6 @@ function PopoverContent({
<Link
to={`${ROUTES.TRACES_EXPLORER}?${relatedTracesLink}`}
className="contributor-row-popover-buttons__button"
data-testid="alert-popover-view-traces"
>
<div className="icon">
<DraftingCompass

View File

@@ -26,10 +26,7 @@ function ChangePercentage({
}: ChangePercentageProps): JSX.Element {
if (direction > 0) {
return (
<div
className="change-percentage change-percentage--success"
data-testid="stats-card-change"
>
<div className="change-percentage change-percentage--success">
<div className="change-percentage__icon">
<ArrowDownLeft size={14} color={Color.BG_FOREST_500} />
</div>
@@ -41,10 +38,7 @@ function ChangePercentage({
}
if (direction < 0) {
return (
<div
className="change-percentage change-percentage--error"
data-testid="stats-card-change"
>
<div className="change-percentage change-percentage--error">
<div className="change-percentage__icon">
<ArrowUpRight size={14} color={Color.BG_CHERRY_500} />
</div>
@@ -56,10 +50,7 @@ function ChangePercentage({
}
return (
<div
className="change-percentage change-percentage--no-previous-data"
data-testid="stats-card-change"
>
<div className="change-percentage change-percentage--no-previous-data">
<div className="change-percentage__label">no previous data</div>
</div>
);
@@ -112,12 +103,7 @@ function StatsCard({
const formattedEndTimeForTooltip = convertTimestampToLocaleDateString(endTime);
return (
<div
className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}
data-testid="stats-card"
data-stats-title={title}
data-empty={isEmpty ? 'true' : 'false'}
>
<div className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}>
<div className="stats-card__title-wrapper">
<div className="title">{title}</div>
<div className="duration-indicator">
@@ -137,7 +123,7 @@ function StatsCard({
</div>
<div className="stats-card__stats">
<div className="count-label" data-testid="stats-card-value">
<div className="count-label">
{isEmpty ? emptyMessage : displayValue || totalCurrentCount}
</div>

View File

@@ -81,11 +81,7 @@ function StatsGraph({ timeSeries, changeDirection }: Props): JSX.Element {
);
return (
<div
style={{ height: '100%', width: '100%' }}
ref={graphRef}
data-testid="stats-card-sparkline"
>
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
<Uplot data={[xData, yData]} options={options} />
</div>
);

View File

@@ -48,16 +48,11 @@ function TopContributorsCard({
return (
<>
<div className="top-contributors-card" data-testid="top-contributors-card">
<div className="top-contributors-card">
<div className="top-contributors-card__header">
<div className="title">top contributors</div>
{topContributorsData.length > 3 && (
<Button
type="text"
className="view-all"
onClick={toggleViewAllDrawer}
data-testid="top-contributors-view-all"
>
<Button type="text" className="view-all" onClick={toggleViewAllDrawer}>
<div className="label">View all</div>
<div className="icon">
<ArrowRight

View File

@@ -68,10 +68,7 @@ function TopContributorsRows({
relatedTracesLink={record.relatedTracesLink}
relatedLogsLink={record.relatedLogsLink}
>
<div
className="total-contribution"
data-testid="top-contributors-row-count"
>
<div className="total-contribution">
{count}/{totalCurrentTriggers}
</div>
</ConditionalAlertPopover>
@@ -81,10 +78,7 @@ function TopContributorsRows({
const handleRowClick = (
record: AlertRuleTopContributors,
): HTMLAttributes<AlertRuleTimelineTableResponse> & {
'data-testid': string;
} => ({
'data-testid': 'top-contributors-row',
): HTMLAttributes<AlertRuleTimelineTableResponse> => ({
onClick: (): void => {
logEvent('Alert history: Top contributors row: Clicked', {
labels: record.labels,

View File

@@ -31,10 +31,7 @@ function ViewAllDrawer({
}}
title="Viewing All Contributors"
>
<div
className="top-contributors-card--view-all"
data-testid="top-contributors-drawer"
>
<div className="top-contributors-card--view-all">
<div className="top-contributors-card__content">
<TopContributorsRows
topContributors={topContributorsData}

View File

@@ -32,8 +32,8 @@ function GraphWrapper({
}, [data?.data]);
return (
<div className="timeline-graph" data-testid="timeline-graph">
<div className="timeline-graph__title" data-testid="timeline-graph-title">
<div className="timeline-graph">
<div className="timeline-graph__title">
{totalCurrentTriggers} triggers in {relativeTime}
</div>
<div className="timeline-graph__chart">

View File

@@ -118,10 +118,7 @@ function TimelineTableContent(): JSX.Element {
const handleRowClick = (
record: AlertRuleTimelineTableResponse,
): HTMLAttributes<AlertRuleTimelineTableResponse> & {
'data-testid': string;
} => ({
'data-testid': 'timeline-row',
): HTMLAttributes<AlertRuleTimelineTableResponse> => ({
onClick: (): void => {
void logEvent('Alert history: Timeline table row: Clicked', {
ruleId: record.ruleID,
@@ -131,15 +128,12 @@ function TimelineTableContent(): JSX.Element {
});
return (
<div className="timeline-table" data-testid="timeline-table">
<div className="timeline-table">
{/* If we don't wait to have the keys, the QuerySearch will not render them at first usage */}
{!isLoadingKeys && hardcodedAttributeKeys ? (
<div className="timeline-table__filter">
<div className="timeline-table__filter-row">
<div
className="timeline-table__filter-search"
data-testid="timeline-filter-search"
>
<div className="timeline-table__filter-search">
<QuerySearch
onChange={querySearchOnChange}
queryData={queryData}
@@ -161,7 +155,6 @@ function TimelineTableContent(): JSX.Element {
<Skeleton.Input
className="timeline-table__filter--loading-skeleton"
active
data-testid="timeline-filter-skeleton"
/>
</div>
)}
@@ -179,17 +172,14 @@ function TimelineTableContent(): JSX.Element {
locale={{
emptyText:
isError && apiError ? (
<div className="timeline-table__error" data-testid="timeline-error">
<div className="timeline-table__error">
<ErrorContent error={apiError} />
</div>
) : undefined,
}}
footer={(): JSX.Element => (
<div className="timeline-table__pagination">
<div
className="timeline-table__pagination-info"
data-testid="timeline-footer-range"
>
<div className="timeline-table__pagination-info">
{paginationConfig.showTotal?.(totalItems, [
totalItems === 0
? 0

View File

@@ -21,14 +21,18 @@ export const timelineTableColumns = ({
sorter: true,
width: 140,
render: (value): JSX.Element => (
<AlertState state={value} showLabel testId="timeline-row-state" />
<div className="alert-rule-state">
<AlertState state={value} showLabel />
</div>
),
},
{
title: 'LABELS',
dataIndex: 'labels',
render: (labels): JSX.Element => (
<AlertLabels labels={labels} testId="timeline-row-labels" />
<div className="alert-rule-labels">
<AlertLabels labels={labels} />
</div>
),
},
{
@@ -36,10 +40,7 @@ export const timelineTableColumns = ({
dataIndex: 'unixMilli',
width: 200,
render: (value): JSX.Element => (
<div
className="alert-rule__created-at"
data-testid="timeline-row-created-at"
>
<div className="alert-rule__created-at">
{formatTimezoneAdjustedTimestamp(value, DATE_TIME_FORMATS.DASH_DATETIME)}
</div>
),
@@ -52,7 +53,7 @@ export const timelineTableColumns = ({
if (!record.relatedTracesLink && !record.relatedLogsLink) {
return (
<Tooltip title="No links available for this item">
<Button type="text" ghost disabled data-testid="timeline-row-actions">
<Button type="text" ghost disabled>
<Ellipsis className="dropdown-icon" size="md" />
</Button>
</Tooltip>
@@ -64,7 +65,7 @@ export const timelineTableColumns = ({
relatedTracesLink={record.relatedTracesLink ?? ''}
relatedLogsLink={record.relatedLogsLink ?? ''}
>
<Button type="text" ghost data-testid="timeline-row-actions">
<Button type="text" ghost>
<Ellipsis className="dropdown-icon" size="md" />
</Button>
</ConditionalAlertPopover>

View File

@@ -23,7 +23,6 @@ function TimelineTabs(): JSX.Element {
{
value: TimelineTab.OVERALL_STATUS,
label: 'Overall Status',
testId: 'timeline-tab-overall-status',
},
{
value: TimelineTab.TOP_5_CONTRIBUTORS,
@@ -34,7 +33,6 @@ function TimelineTabs(): JSX.Element {
</div>
),
disabled: true,
testId: 'timeline-tab-top-contributors',
},
];
@@ -59,17 +57,14 @@ function TimelineFilters(): JSX.Element {
{
value: TimelineFilter.ALL,
label: 'All',
testId: 'timeline-filter-all',
},
{
value: TimelineFilter.FIRED,
label: 'Fired',
testId: 'timeline-filter-fired',
},
{
value: TimelineFilter.RESOLVED,
label: 'Resolved',
testId: 'timeline-filter-resolved',
},
];

View File

@@ -6,7 +6,7 @@ import {
getAllEndpointsWidgetData,
getGroupByFiltersFromGroupByValues,
} from 'container/ApiMonitoring/utils';
import GridCard from 'container/GridCardLayout/GridCard';
import GridCard from 'container/WidgetCard/Card';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { isEqual } from 'lodash-es';

View File

@@ -1,7 +1,7 @@
import { Card } from 'antd';
import { ENTITY_VERSION_V5 } from 'constants/app';
import GridCard from 'container/GridCardLayout/GridCard';
import { Widgets } from 'types/api/dashboard/getAll';
import GridCard from 'container/WidgetCard/Card';
import { Widgets } from 'types/api/widgets/widget';
function MetricOverTimeGraph({
widget,

View File

@@ -11,10 +11,10 @@ import {
getStatusCodeBarChartWidgetData,
statusCodeWidgetInfo,
} from 'container/ApiMonitoring/utils';
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
import { handleGraphClick } from 'container/WidgetCard/Card/utils';
import { useGraphClickToShowButton } from 'container/WidgetCard/hooks/useGraphClickToShowButton';
import useNavigateToExplorerPages from 'container/WidgetCard/hooks/useNavigateToExplorerPages';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@@ -23,7 +23,7 @@ import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { useTimezone } from 'providers/Timezone';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import ErrorState from './ErrorState';

View File

@@ -1,7 +1,7 @@
import { ExecStats } from 'api/v5/v5';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';

View File

@@ -17,7 +17,7 @@ jest.mock('container/ApiMonitoring/utils', () => ({
getGroupByFiltersFromGroupByValues: jest.fn(),
}));
jest.mock('container/GridCardLayout/GridCard', () => ({
jest.mock('container/WidgetCard/Card', () => ({
__esModule: true,
default: jest.fn().mockImplementation(({ customOnRowClick }) => (
<div data-testid="grid-card-mock">

View File

@@ -21,15 +21,12 @@ interface MockQueryResult {
}
// Mocks
jest.mock(
'container/DashboardContainer/visualization/charts/BarChart/BarChart',
() => ({
__esModule: true,
default: jest
.fn()
.mockImplementation(() => <div data-testid="bar-chart-mock" />),
}),
);
jest.mock('lib/visualization/charts/BarChart/BarChart', () => ({
__esModule: true,
default: jest
.fn()
.mockImplementation(() => <div data-testid="bar-chart-mock" />),
}));
jest.mock('components/CeleryTask/useGetGraphCustomSeries', () => ({
useGetGraphCustomSeries: (): { getCustomSeries: jest.Mock } => ({
@@ -43,7 +40,7 @@ jest.mock('components/CeleryTask/useNavigateToExplorer', () => ({
}),
}));
jest.mock('container/GridCardLayout/useGraphClickToShowButton', () => ({
jest.mock('container/WidgetCard/hooks/useGraphClickToShowButton', () => ({
useGraphClickToShowButton: (): {
componentClick: boolean;
htmlRef: HTMLElement | null;
@@ -53,7 +50,7 @@ jest.mock('container/GridCardLayout/useGraphClickToShowButton', () => ({
}),
}));
jest.mock('container/GridCardLayout/useNavigateToExplorerPages', () => ({
jest.mock('container/WidgetCard/hooks/useNavigateToExplorerPages', () => ({
__esModule: true,
default: (): { navigateToExplorerPages: jest.Mock } => ({
navigateToExplorerPages: jest.fn(),

View File

@@ -10,7 +10,7 @@ import {
} from 'components/QuickFilters/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { GraphClickMetaData } from 'container/GridCardLayout/useNavigateToExplorerPages';
import { GraphClickMetaData } from 'container/WidgetCard/hooks/useNavigateToExplorerPages';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { convertNanoToMilliseconds } from 'container/MetricsExplorer/Summary/utils';
import dayjs from 'dayjs';
@@ -18,7 +18,7 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { cloneDeep } from 'lodash-es';
import { ArrowUpDown, ChevronDown, ChevronRight, Info } from '@signozhq/icons';
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import {
BaseAutocompleteData,

View File

@@ -1,10 +1,11 @@
import { useCallback, useMemo, useRef } from 'react';
import { Card, Flex } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
import { StackMode } from 'lib/uPlotV2/config/types';
import {
LegendPosition,
TooltipRenderArgs,
@@ -131,9 +132,9 @@ export function BillingUsageGraph(props: BillingUsageGraphProps): JSX.Element {
<div ref={graphRef} className={styles.graphContainer}>
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
<BarChart
stack={StackMode.Normal}
config={config}
data={chartData}
isStackedBarChart
legendConfig={{ position: LegendPosition.BOTTOM }}
customTooltip={renderBillingTooltip}
width={containerDimensions.width}

View File

@@ -58,26 +58,17 @@ describe('prepareBillingBarConfig', () => {
expect(config.series?.[4]?.stroke).toBe(Color.BG_AMBER_500);
});
it('sets stacking bands, padding, and focus alpha for behavioral parity', () => {
it('sets padding and focus alpha for behavioral parity', () => {
const builder = prepareBillingBarConfig({
...baseProps,
apiResponse: makeApiResponse(['Logs', 'Traces', 'Metrics']),
});
const config = builder.getConfig();
expect(config.bands).toStrictEqual([{ series: [1, 2] }, { series: [2, 3] }]);
// Stacking bands come from the chart now — see useChartStacking.
expect(config.padding).toStrictEqual([32, 32, 16, 16]);
expect(config.focus).toStrictEqual({ alpha: 0.3 });
});
it('sets no bands when result is empty', () => {
const builder = prepareBillingBarConfig({
...baseProps,
apiResponse: makeApiResponse([]),
});
const config = builder.getConfig();
expect(config.bands).toBeUndefined();
});
it('uses queryName as label when legend is undefined', () => {
const apiResponse: MetricRangePayloadProps = {
data: {

View File

@@ -1,8 +1,7 @@
import { Color } from '@signozhq/design-tokens';
import type { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getInitialStackedBands } from 'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils';
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
import { DrawStyle } from 'lib/uPlotV2/config/types';
import type { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import type { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
@@ -63,7 +62,6 @@ export function prepareBillingBarConfig({
});
});
builder.setBands(getInitialStackedBands(results.length));
builder.setPadding([32, 32, 16, 16]);
builder.setFocus({ alpha: 0.3 });

View File

@@ -98,7 +98,7 @@ jest.mock('api/channels/getAll', () => ({
}));
// Mock alert format categories
jest.mock('container/NewWidget/RightContainer/alertFomatCategories', () => ({
jest.mock('constants/formats/alertFormatCategories', () => ({
getCategoryByOptionId: jest.fn(() => ({ name: 'bytes' })),
getCategorySelectOptionByName: jest.fn(() => [
{ label: 'Bytes', value: 'bytes' },

View File

@@ -34,7 +34,6 @@ function AdvancedOptions(): JSX.Element {
})
}
value={advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit}
testId="send-notification-if-data-is-missing-input"
/>
<Typography.Text>Minutes</Typography.Text>
</div>
@@ -67,7 +66,6 @@ function AdvancedOptions(): JSX.Element {
})
}
value={advancedOptions.enforceMinimumDatapoints.minimumDatapoints}
testId="enforce-minimum-datapoints-input"
/>
<Typography.Text>Datapoints</Typography.Text>
</div>

View File

@@ -66,7 +66,6 @@ function EvaluationWindowPopover({
tabIndex={0}
data-value={option.value}
data-section-id={sectionId}
data-testid={`${sectionId}-option-${option.value}`}
onClick={(): void => onChange(option.value)}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {

View File

@@ -186,7 +186,6 @@ function Footer(): JSX.Element {
color="primary"
onClick={handleSaveAlert}
disabled={disableButtons || Boolean(alertValidationMessage)}
testId="save-alert-rule-button"
>
{isCreatingAlertRule || isUpdatingAlertRule ? (
<Loader data-testid="save-alert-rule-loader-icon" size={14} />
@@ -219,7 +218,6 @@ function Footer(): JSX.Element {
color="secondary"
onClick={handleTestNotification}
disabled={disableButtons || Boolean(alertValidationMessage)}
testId="test-notification-button"
>
{isTestingAlertRule ? (
<Loader data-testid="test-notification-loader-icon" size={14} />
@@ -251,7 +249,6 @@ function Footer(): JSX.Element {
color="secondary"
onClick={handleDiscard}
disabled={disableButtons}
testId="discard-alert-rule-button"
>
<X size={14} /> Discard
</Button>

View File

@@ -8,7 +8,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryParams } from 'constants/query';
import { useCreateAlertState } from 'container/CreateAlertV2/context';
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import PlotTag from 'components/PlotTag/PlotTag';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
import { AppState } from 'store/reducers';

View File

@@ -44,7 +44,7 @@ jest.mock(
},
);
jest.mock(
'container/NewWidget/LeftContainer/WidgetGraph/PlotTag',
'components/PlotTag/PlotTag',
() =>
function MockPlotTag(props: any): JSX.Element {
return (

View File

@@ -1,440 +0,0 @@
.dashboard-description-container {
box-shadow: none;
border: none;
background: unset;
box-shadow: none;
color: var(--l2-foreground);
.ant-card-body {
padding: 0px;
}
.dashboard-details {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 16px 16px 0px 16px;
align-items: flex-start;
.left-section {
display: flex;
align-items: center;
gap: 8px;
width: 45%;
.dashboard-img {
height: 16px;
width: 16px;
}
.dashboard-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px; /* 150% */
letter-spacing: -0.08px;
max-width: 80%;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.public-dashboard-icon {
margin-right: 4px;
}
}
.right-section {
display: flex;
width: 55%;
justify-content: flex-end;
flex-wrap: wrap;
align-items: center;
gap: 14px;
.icons {
display: flex;
align-items: center;
width: 32px;
height: 34px;
padding: 6px;
justify-content: center;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px;
}
.icons:hover {
background-color: unset;
}
.configure-button {
display: flex;
align-items: center;
width: 93px;
height: 34px;
padding: 6px;
justify-content: center;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px;
}
.add-panel-btn {
display: flex;
width: 119px;
height: 34px;
padding: 5.937px 11.875px;
justify-content: center;
align-items: center;
color: var(--primary-foreground);
background: var(--primary-background);
font-family: Inter;
font-size: 11.875px;
font-style: normal;
font-weight: 500;
line-height: 17.812px; /* 150% */
}
}
}
.dashboard-tags {
display: flex;
gap: 6px;
padding: 16px 16px 0px 16px;
flex-wrap: wrap;
.tag {
display: flex;
padding: 4px 8px;
justify-content: center;
align-items: center;
border-radius: 20px;
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
color: var(--bg-sienna-400);
text-align: center;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
margin-inline-end: 0px;
}
}
.dashboard-description-section {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
padding: 20px 16px 0px 16px;
/* Preserve author-entered line breaks in the description. */
white-space: pre-wrap;
overflow-wrap: anywhere;
a {
color: var(--accent-primary);
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
}
.dashboard-variables {
padding: 16px 16px 18px 16px;
}
}
.dashboard-description {
display: -webkit-box;
-webkit-line-clamp: 2; /* Show up to 2 lines */
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
}
.dashboard-settings {
width: 191px;
height: 302px;
flex-shrink: 0;
.ant-popover-inner {
padding: 0px;
border-radius: 4px;
border: 1px solid var(--l1-border);
background: linear-gradient(
139deg,
color-mix(in srgb, var(--card) 80%, transparent) 0%,
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
) !important;
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
}
.menu-content {
display: flex;
flex-direction: column;
section {
display: flex;
flex-direction: column;
align-items: start;
.ant-btn {
display: flex;
width: 100%;
height: unset;
padding: 8px;
align-items: center;
gap: 12px;
color: var(--l2-foreground);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: 0.14px;
border-top: none;
.ant-btn-icon {
margin-inline-end: 0px;
}
}
}
.section-1,
.section-2 {
border-bottom: 1px solid var(--l1-border);
}
.delete-dashboard .ant-btn {
color: var(--bg-cherry-400) !important;
}
}
}
.rename-dashboard {
.ant-modal-content {
width: 384px;
flex-shrink: 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);
padding: 0px;
.ant-modal-header {
height: 52px;
padding: 16px;
background: var(--l2-background);
border-bottom: 1px solid var(--l1-border);
margin-bottom: 0px;
.ant-modal-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
width: 349px;
height: 20px;
}
}
.ant-modal-body {
padding: 16px;
.dashboard-content {
display: flex;
flex-direction: column;
gap: 8px;
.name-text {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
}
.dashboard-name-input {
display: flex;
padding: 6px 6px 6px 8px;
align-items: center;
gap: 4px;
align-self: stretch;
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
}
}
}
.ant-modal-footer {
padding: 16px;
margin-top: 0px;
.dashboard-rename {
display: flex;
flex-direction: row-reverse;
gap: 12px;
.cancel-btn {
display: flex;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 2px;
background: var(--l1-border);
.ant-btn-icon {
margin-inline-end: 0px;
}
}
.rename-btn {
display: flex;
align-items: center;
display: flex;
width: 169px;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 2px;
background: var(--primary-background);
.ant-btn-icon {
margin-inline-end: 0px;
}
}
}
}
}
}
.section-naming {
.ant-modal-content {
width: 384px;
flex-shrink: 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);
padding: 0px;
.ant-modal-header {
height: 52px;
padding: 16px;
background: var(--l2-background);
border-bottom: 1px solid var(--l1-border);
margin-bottom: 0px;
.ant-modal-title {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
width: 349px;
height: 20px;
}
}
.ant-modal-body {
padding: 16px;
.section-naming-content {
display: flex;
flex-direction: column;
gap: 8px;
.name-text {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
}
.section-name-input {
display: flex;
width: 320px;
padding: 6px 6px 6px 8px;
align-items: center;
gap: 4px;
align-self: stretch;
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
}
}
}
.ant-modal-footer {
padding: 16px;
margin-top: 0px;
.dashboard-rename {
display: flex;
flex-direction: row-reverse;
gap: 12px;
.cancel-btn {
display: flex;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 2px;
background: var(--l1-border);
.ant-btn-icon {
margin-inline-end: 0px;
}
}
.rename-btn {
display: flex;
align-items: center;
display: flex;
width: 140px;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 2px;
background: var(--primary-background);
.ant-btn-icon {
margin-inline-end: 0px;
}
}
}
}
}
}

View File

@@ -1,43 +0,0 @@
.settings-container-root {
.ant-drawer-wrapper-body {
border-left: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
.ant-drawer-header {
height: 48px;
border-bottom: 1px solid var(--l1-border);
padding: 14px 14px 14px 11px;
.ant-drawer-header-title {
gap: 16px;
.ant-drawer-title {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
padding-left: 16px;
border-left: 1px solid var(--l1-border);
}
.ant-drawer-close {
height: 16px;
width: 16px;
margin-inline-end: 0px !important;
}
}
}
.ant-drawer-body {
padding: 16px;
&::-webkit-scrollbar {
width: 0.1rem;
}
}
}
}

View File

@@ -1,34 +0,0 @@
import { memo, PropsWithChildren, ReactElement } from 'react';
import { Drawer } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import './SettingsDrawer.styles.scss';
type SettingsDrawerProps = PropsWithChildren<{
drawerTitle: string;
isOpen: boolean;
onClose: () => void;
}>;
function SettingsDrawer({
children,
drawerTitle,
isOpen,
onClose,
}: SettingsDrawerProps): JSX.Element {
return (
<Drawer
title={drawerTitle}
placement="right"
width="50%"
onClose={onClose}
open={isOpen}
rootClassName="settings-container-root"
>
{/* Need to type cast because of OverlayScrollbar type definition. We should be good once we remove it. */}
<OverlayScrollbar>{children as ReactElement}</OverlayScrollbar>
</Drawer>
);
}
export default memo(SettingsDrawer);

View File

@@ -1,235 +0,0 @@
import { ReactNode } from 'react';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
import {
getDashboardById,
getNonIntegrationDashboardById,
} from 'mocks-server/__mockdata__/dashboards';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import {
resetDashboard,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import {
fireEvent,
render,
screen,
userEvent,
waitFor,
} from 'tests/test-utils';
import { Dashboard } from 'types/api/dashboard/getAll';
import DashboardDescription from '..';
function DashboardBootstrapWrapper({
dashboardId,
children,
}: {
dashboardId: string;
children: ReactNode;
}): JSX.Element {
useDashboardBootstrap(dashboardId);
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
interface MockSafeNavigateReturn {
safeNavigate: jest.MockedFunction<(url: string) => void>;
}
const DASHBOARD_TEST_ID = 'dashboard-title';
const DASHBOARD_TITLE_TEXT = 'thor';
const DASHBOARD_PATH = '/dashboard/4';
const mockSafeNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
}));
jest.mock(
'container/TopNav/DateTimeSelectionV2/index.tsx',
() =>
function MockDateTimeSelection(): JSX.Element {
return <div>MockDateTimeSelection</div>;
},
);
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): MockSafeNavigateReturn => ({
safeNavigate: mockSafeNavigate,
}),
}));
describe('Dashboard landing page actions header tests', () => {
beforeEach(() => {
mockSafeNavigate.mockClear();
sessionStorage.clear();
resetDashboard();
});
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}${DASHBOARD_PATH}`,
search: '',
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByTestId } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardBootstrapWrapper dashboardId="4">
<DashboardDescription
handle={{
active: false,
enter: (): Promise<void> => Promise.resolve(),
exit: (): Promise<void> => Promise.resolve(),
node: { current: null },
}}
/>
</DashboardBootstrapWrapper>
</MemoryRouter>,
);
await waitFor(() =>
expect(getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent(
DASHBOARD_TITLE_TEXT,
),
);
const dashboardSettingsTrigger = getByTestId('options');
await fireEvent.click(dashboardSettingsTrigger);
const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard');
await waitFor(() => expect(lockUnlockButton).toBeDisabled());
});
it('unlock dashboard should not be disabled for non integration created dashboards', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}${DASHBOARD_PATH}`,
search: '',
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
server.use(
rest.get('http://localhost/api/v1/dashboards/4', (_, res, ctx) =>
res(ctx.status(200), ctx.json(getNonIntegrationDashboardById)),
),
);
const { getByTestId } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardBootstrapWrapper dashboardId="4">
<DashboardDescription
handle={{
active: false,
enter: (): Promise<void> => Promise.resolve(),
exit: (): Promise<void> => Promise.resolve(),
node: { current: null },
}}
/>
</DashboardBootstrapWrapper>
</MemoryRouter>,
);
await waitFor(() =>
expect(getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent(
DASHBOARD_TITLE_TEXT,
),
);
const dashboardSettingsTrigger = getByTestId('options');
await fireEvent.click(dashboardSettingsTrigger);
const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard');
await waitFor(() => expect(lockUnlockButton).not.toBeDisabled());
});
it('should navigate to base dashboard list URL when no saved params exist', async () => {
const user = userEvent.setup();
const mockLocation = {
pathname: DASHBOARD_PATH,
search: '',
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByText } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardBootstrapWrapper dashboardId="4">
<DashboardDescription
handle={{
active: false,
enter: (): Promise<void> => Promise.resolve(),
exit: (): Promise<void> => Promise.resolve(),
node: { current: null },
}}
/>
</DashboardBootstrapWrapper>
</MemoryRouter>,
);
await waitFor(() =>
expect(screen.getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent(
DASHBOARD_TITLE_TEXT,
),
);
const dashboardButton = getByText('Dashboard /');
await user.click(dashboardButton);
expect(mockSafeNavigate).toHaveBeenCalledWith('/dashboard');
});
it('should navigate to dashboard list with saved query params when present', async () => {
const user = userEvent.setup();
const savedParams = 'columnKey=createdAt&order=ascend&page=2&search=foo';
sessionStorage.setItem('dashboardsListQueryParams', savedParams);
const mockLocation = {
pathname: DASHBOARD_PATH,
search: '',
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
useDashboardStore.setState({
dashboardData: getDashboardById.data as unknown as Dashboard,
layouts: [],
panelMap: {},
setPanelMap: jest.fn(),
setLayouts: jest.fn(),
setDashboardData: jest.fn(),
columnWidths: {},
});
const { getByText } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardDescription
handle={{
active: false,
enter: (): Promise<void> => Promise.resolve(),
exit: (): Promise<void> => Promise.resolve(),
node: { current: null },
}}
/>
</MemoryRouter>,
);
await waitFor(() =>
expect(screen.getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent(
DASHBOARD_TITLE_TEXT,
),
);
const dashboardButton = getByText('Dashboard /');
await user.click(dashboardButton);
expect(mockSafeNavigate).toHaveBeenCalledWith({
pathname: '/dashboard',
search: `?${savedParams}`,
});
});
});

View File

@@ -1,621 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { FullScreenHandle } from 'react-full-screen';
import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
import { useCopyToClipboard } from 'react-use';
import {
Check,
ClipboardCopy,
Ellipsis,
FileJson,
FolderKanban,
Fullscreen,
Globe,
LockKeyhole,
PenLine,
Plus,
X,
} from '@signozhq/icons';
import { Button, Card, Modal, Popover, Tooltip } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Typography } from '@signozhq/ui/typography';
import { Input } from '@signozhq/ui/input';
import logEvent from 'api/common/logEvent';
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
import { useLockDashboard } from 'hooks/dashboard/useLockDashboard';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import {
selectIsDashboardLocked,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { sortLayout } from 'providers/Dashboard/util';
import { DashboardData } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { ROLES, USER_ROLES } from 'types/roles';
import { linkifyText } from 'utils/linkifyText';
import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid';
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
import DashboardSettings from '../DashboardSettings';
import { Base64Icons } from '../DashboardSettings/General/utils';
import DashboardVariableSelection from '../DashboardVariablesSelection';
import PanelTypeSelectionModal from '../PanelTypeSelectionModal';
import SettingsDrawer from './SettingsDrawer';
import { VariablesSettingsTab } from './types';
import {
DEFAULT_ROW_NAME,
downloadObjectAsJson,
sanitizeDashboardData,
} from './utils';
import './Description.styles.scss';
interface DashboardDescriptionProps {
handle: FullScreenHandle;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { handle } = props;
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const {
dashboardData,
panelMap,
setPanelMap,
layouts,
setLayouts,
setDashboardData,
} = useDashboardStore();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const handleDashboardLockToggle = useLockDashboard();
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] =
useState<boolean>(false);
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
const isPublicDashboardEnabled = isCloudUser || isEnterpriseSelfHostedUser;
const selectedData = dashboardData
? {
...dashboardData.data,
uuid: dashboardData.id,
}
: ({} as DashboardData);
const { dashboardVariables } = useDashboardVariables();
const {
title = '',
description,
tags,
image = Base64Icons[0],
} = selectedData || {};
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
const [sectionName, setSectionName] = useState<string>(DEFAULT_ROW_NAME);
const updateDashboardMutation = useUpdateDashboard();
const { user } = useAppContext();
const [editDashboard] = useComponentPermission(['edit_dashboard'], user.role);
const [isDashboardSettingsOpen, setIsDashbordSettingsOpen] =
useState<boolean>(false);
const [isRenameDashboardOpen, setIsRenameDashboardOpen] =
useState<boolean>(false);
const [isPanelNameModalOpen, setIsPanelNameModalOpen] =
useState<boolean>(false);
const [isPublicDashboard, setIsPublicDashboard] = useState<boolean>(false);
let isAuthor = false;
if (dashboardData && user && user.email) {
isAuthor = dashboardData?.createdBy === user?.email;
}
let permissions: ComponentTypes[] = ['add_panel'];
if (isDashboardLocked) {
permissions = ['add_panel_locked_dashboard'];
}
const { notifications } = useNotifications();
const userRole: ROLES | null =
dashboardData?.createdBy === user?.email
? (USER_ROLES.AUTHOR as ROLES)
: user.role;
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setIsPanelTypeSelectionModalOpen(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: dashboardData?.id,
dashboardName: dashboardData?.data.title,
numberOfPanels: dashboardData?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIsPanelTypeSelectionModalOpen]);
const handleLockDashboardToggle = (): void => {
setIsDashbordSettingsOpen(false);
handleDashboardLockToggle(!isDashboardLocked);
};
const onNameChangeHandler = (): void => {
if (!dashboardData) {
return;
}
const updatedDashboard: Props = {
id: dashboardData.id,
data: {
...dashboardData.data,
title: updatedTitle,
},
};
updateDashboardMutation.mutate(updatedDashboard, {
onSuccess: (updatedDashboard) => {
notifications.success({
message: 'Dashboard renamed successfully',
});
setIsRenameDashboardOpen(false);
if (updatedDashboard.data) {
setDashboardData(updatedDashboard.data);
}
},
onError: () => {
setIsRenameDashboardOpen(true);
},
});
};
const [state, setCopy] = useCopyToClipboard();
const { t } = useTranslation(['dashboard', 'common']);
// used to set the initial value for the updatedTitle
// the context value is sometimes not available during the initial render
// due to which the updatedTitle is set to some previous value
useEffect(() => {
if (dashboardData) {
setUpdatedTitle(dashboardData.data.title);
}
}, [dashboardData]);
useEffect(() => {
if (state.error) {
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
});
}
if (state.value) {
notifications.success({
message: t('success', {
ns: 'common',
}),
});
}
}, [state.error, state.value, t, notifications]);
function handleAddRow(): void {
if (!dashboardData) {
return;
}
const id = uuid();
const newRowWidgetMap: { widgets: Layout[]; collapsed: boolean } = {
widgets: [],
collapsed: false,
};
const currentRowIdx = 0;
for (let j = currentRowIdx; j < layouts.length; j++) {
if (!panelMap[layouts[j].i]) {
newRowWidgetMap.widgets.push(layouts[j]);
} else {
break;
}
}
const updatedDashboard: Props = {
id: dashboardData.id,
data: {
...dashboardData.data,
layout: [
{
i: id,
w: 12,
minW: 12,
minH: 1,
maxH: 1,
x: 0,
h: 1,
y: 0,
},
...layouts.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
],
panelMap: { ...panelMap, [id]: newRowWidgetMap },
widgets: [
...(dashboardData.data.widgets || []),
{
id,
title: sectionName,
description: '',
panelTypes: PANEL_GROUP_TYPES.ROW,
},
],
},
};
updateDashboardMutation.mutate(updatedDashboard, {
onSuccess: (updatedDashboard) => {
if (updatedDashboard.data) {
if (updatedDashboard.data.data.layout) {
setLayouts(sortLayout(updatedDashboard.data.data.layout));
}
setDashboardData(updatedDashboard.data);
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
}
setIsPanelNameModalOpen(false);
setSectionName(DEFAULT_ROW_NAME);
},
});
}
const {
data: publicDashboardResponse,
isLoading: isLoadingPublicDashboardData,
isFetching: isFetchingPublicDashboardData,
error: errorPublicDashboardData,
isError: isErrorPublicDashboardData,
} = useGetPublicDashboardMeta(
dashboardData?.id || '',
!!dashboardData?.id && isPublicDashboardEnabled,
);
useEffect(() => {
if (!isLoadingPublicDashboardData && !isFetchingPublicDashboardData) {
if (isErrorPublicDashboardData) {
const errorDetails = errorPublicDashboardData?.getErrorDetails();
if (errorDetails?.error?.code === 'public_dashboard_not_found') {
setIsPublicDashboard(false);
}
} else {
const publicDashboardData = publicDashboardResponse?.data;
if (publicDashboardData?.publicPath) {
setIsPublicDashboard(true);
}
}
}
}, [
isLoadingPublicDashboardData,
isFetchingPublicDashboardData,
isErrorPublicDashboardData,
errorPublicDashboardData,
publicDashboardResponse?.data,
]);
const onConfigureClick = useCallback((): void => {
setIsSettingsDrawerOpen(true);
}, []);
const onSettingsDrawerClose = useCallback((): void => {
setIsSettingsDrawerOpen(false);
// good use case for a state library like Jotai
if (variablesSettingsTabHandle.current) {
variablesSettingsTabHandle.current.resetState();
}
}, []);
return (
<Card className="dashboard-description-container">
<DashboardHeader />
<section className="dashboard-details">
<div className="left-section">
<img src={image} alt="dashboard-img" className="dashboard-img" />
<Tooltip title={title.length > 30 ? title : ''}>
<Typography.Text
className="dashboard-title"
data-testid="dashboard-title"
>
{title}
</Typography.Text>
</Tooltip>
{isPublicDashboard && (
<Tooltip title="This dashboard is publicly accessible">
<Globe size={14} className="public-dashboard-icon" />
</Tooltip>
)}
{isDashboardLocked && (
<Tooltip title="This dashboard is locked">
<LockKeyhole size={14} className="lock-dashboard-icon" />
</Tooltip>
)}
</div>
<div className="right-section">
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
<Popover
open={isDashboardSettingsOpen}
arrow={false}
onOpenChange={(visible): void => setIsDashbordSettingsOpen(visible)}
rootClassName="dashboard-settings"
content={
<div className="menu-content">
<section className="section-1">
{(isAuthor || user.role === USER_ROLES.ADMIN) && (
<Tooltip
title={
dashboardData?.createdBy === 'integration' &&
'Dashboards created by integrations cannot be unlocked'
}
>
<Button
type="text"
icon={<LockKeyhole size={14} />}
disabled={dashboardData?.createdBy === 'integration'}
onClick={handleLockDashboardToggle}
data-testid="lock-unlock-dashboard"
>
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
</Button>
</Tooltip>
)}
{!isDashboardLocked && editDashboard && (
<Button
type="text"
icon={<PenLine size={14} />}
onClick={(): void => {
setIsRenameDashboardOpen(true);
setIsDashbordSettingsOpen(false);
}}
>
Rename
</Button>
)}
<Button
type="text"
icon={<Fullscreen size={14} />}
onClick={handle.enter}
>
Full screen
</Button>
</section>
<section className="section-2">
{!isDashboardLocked && addPanelPermission && (
<Button
type="text"
icon={<FolderKanban size={14} />}
onClick={(): void => {
setIsPanelNameModalOpen(true);
setIsDashbordSettingsOpen(false);
}}
>
New section
</Button>
)}
<Button
type="text"
icon={<FileJson size={14} />}
onClick={(): void => {
downloadObjectAsJson(
sanitizeDashboardData(selectedData),
selectedData.title,
);
setIsDashbordSettingsOpen(false);
}}
>
Export JSON
</Button>
<Button
type="text"
icon={<ClipboardCopy size={14} />}
onClick={(): void => {
setCopy(
JSON.stringify(sanitizeDashboardData(selectedData), null, 2),
);
setIsDashbordSettingsOpen(false);
}}
>
Copy as JSON
</Button>
</section>
<section className="delete-dashboard">
<DeleteButton
createdBy={dashboardData?.createdBy || ''}
name={dashboardData?.data.title || ''}
id={String(dashboardData?.id) || ''}
isLocked={isDashboardLocked}
routeToListPage
/>
</section>
</div>
}
trigger="click"
placement="bottomRight"
>
<Button
icon={<Ellipsis size={14} />}
type="text"
className="icons"
data-testid="options"
/>
</Popover>
{!isDashboardLocked && editDashboard && (
<>
<Button
type="text"
className="configure-button"
icon={<ConfigureIcon />}
data-testid="show-drawer"
onClick={onConfigureClick}
>
Configure
</Button>
<SettingsDrawer
drawerTitle="Dashboard Configuration"
isOpen={isSettingsDrawerOpen}
onClose={onSettingsDrawerClose}
>
<DashboardSettings
variablesSettingsTabHandle={variablesSettingsTabHandle}
/>
</SettingsDrawer>
</>
)}
{!isDashboardLocked && addPanelPermission && (
<Button
className="add-panel-btn"
onClick={onEmptyWidgetHandler}
icon={<Plus size="md" />}
type="primary"
data-testid="add-panel-header"
>
New Panel
</Button>
)}
</div>
</section>
{(tags?.length || 0) > 0 && (
<div className="dashboard-tags">
{tags?.map((tag) => (
<Badge key={tag} className="tag" color="vanilla">
{tag}
</Badge>
))}
</div>
)}
{!isEmpty(description) && (
<section className="dashboard-description-section">
{linkifyText(description ?? '')}
</section>
)}
{!isEmpty(dashboardVariables) && (
<section className="dashboard-variables">
<DashboardVariableSelection />
</section>
)}
<PanelTypeSelectionModal />
<Modal
open={isRenameDashboardOpen}
title="Rename Dashboard"
onOk={(): void => {
// handle update dashboard here
}}
onCancel={(): void => {
setIsRenameDashboardOpen(false);
}}
rootClassName="rename-dashboard"
footer={
<div className="dashboard-rename">
<Button
type="primary"
icon={<Check size={14} />}
className="rename-btn"
onClick={onNameChangeHandler}
disabled={updateDashboardMutation.isLoading}
>
Rename Dashboard
</Button>
<Button
type="text"
icon={<X size={14} />}
className="cancel-btn"
onClick={(): void => setIsRenameDashboardOpen(false)}
>
Cancel
</Button>
</div>
}
>
<div className="dashboard-content">
<Typography.Text className="name-text">Enter a new name</Typography.Text>
<Input
data-testid="dashboard-name"
className="dashboard-name-input"
value={updatedTitle}
onChange={(e): void => setUpdatedTitle(e.target.value)}
/>
</div>
</Modal>
<Modal
open={isPanelNameModalOpen}
title="New Section"
rootClassName="section-naming"
onOk={(): void => handleAddRow()}
onCancel={(): void => {
setIsPanelNameModalOpen(false);
setSectionName(DEFAULT_ROW_NAME);
}}
footer={
<div className="dashboard-rename">
<Button
type="primary"
icon={<Check size={14} />}
className="rename-btn"
onClick={(): void => handleAddRow()}
disabled={updateDashboardMutation.isLoading}
>
Create Section
</Button>
<Button
type="text"
icon={<X size={14} />}
className="cancel-btn"
onClick={(): void => {
setIsPanelNameModalOpen(false);
setSectionName(DEFAULT_ROW_NAME);
}}
>
Cancel
</Button>
</div>
}
>
<div className="section-naming-content">
<Typography.Text className="name-text">Enter Section name</Typography.Text>
<Input
data-testid="section-name"
className="section-name-input"
value={sectionName}
onChange={(e): void => setSectionName(e.target.value)}
/>
</div>
</Modal>
</Card>
);
}
export default DashboardDescription;

View File

@@ -1,8 +0,0 @@
import { MutableRefObject } from 'react';
export interface VariablesSettingsTab {
resetState: () => void;
}
export type VariablesSettingsTabHandle =
MutableRefObject<VariablesSettingsTab | null>;

View File

@@ -1,40 +0,0 @@
import { DashboardData, IDashboardVariable } from 'types/api/dashboard/getAll';
export function sanitizeDashboardData(
selectedData: DashboardData,
): DashboardData {
if (!selectedData?.variables) {
return selectedData;
}
const updatedVariables = Object.entries(selectedData.variables).reduce(
(acc, [key, value]) => {
const { selectedValue: _selectedValue, ...rest } = value;
acc[key] = rest;
return acc;
},
{} as Record<string, IDashboardVariable>,
);
return {
...selectedData,
variables: updatedVariables,
};
}
export function downloadObjectAsJson(
exportObj: unknown,
exportName: string,
): void {
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(exportObj),
)}`;
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute('href', dataStr);
downloadAnchorNode.setAttribute('download', `${exportName}.json`);
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
export const DEFAULT_ROW_NAME = 'Sample Row';

View File

@@ -1,123 +0,0 @@
.delete-variable-name {
font-weight: 700;
color: rgb(207, 19, 34);
font-style: italic;
}
.apply-to-all-variable-name {
font-weight: 700;
color: var(--bg-robin-400);
font-style: italic;
}
.dashboard-variable-settings-table {
.variable-name-drag {
display: flex;
align-items: center;
gap: 10px;
.ant-table-cell {
padding: 0px !important;
border: none !important;
color: var(--bg-robin-400);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
}
.variable-description-actions {
display: flex;
align-items: center;
gap: 10px;
flex: 1 0 0;
.variable-description {
color: var(--bg-sienna-400);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
}
.actions-btns {
position: absolute;
right: 10px;
display: none;
&:hover {
display: inline-flex;
}
.edit-variable-button {
width: 26px;
height: 22px;
border-radius: 2px;
display: flex;
padding: 4px 6px;
align-items: center;
gap: 3px;
background: var(--l1-border);
}
.delete-variable-button {
width: 26px;
height: 22px;
border-radius: 2px;
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
display: flex;
padding: 4px 6px;
align-items: center;
gap: 3px;
color: red;
}
.apply-to-all-button {
width: min-content;
height: 22px;
border-radius: 2px;
display: flex;
padding: 0px 6px;
align-items: center;
gap: 3px;
background: var(--l1-border);
}
}
}
.ant-table-cell-row-hover {
.actions-btns {
display: inline-flex;
}
}
.ant-table-thead {
.ant-table-cell {
padding: 8px 12px;
border: 1px solid var(--l1-border);
background: unset;
}
.ant-table-cell::before {
display: none;
}
}
.ant-table-tbody {
.ant-table-cell {
padding: 14px;
border: 1px solid var(--l1-border);
}
}
.ant-table-row {
.ant-table-cell:nth-child(even) {
background: color-mix(in srgb, var(--l1-border) 40%, transparent);
}
}
}

View File

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

View File

@@ -1,31 +0,0 @@
.dynamic-variable-container {
margin: 24px 0;
.dynamic-variable-config-container {
display: grid;
grid-template-columns: 1fr 32px 16px 200px;
gap: 16px;
align-items: center;
width: 100%;
.ant-select {
.ant-select-selector {
border-radius: 4px;
border: 1px solid var(--l1-border);
}
}
.ant-input {
border-radius: 4px;
border: 1px solid var(--l1-border);
max-width: 300px;
}
.dynamic-variable-from-text {
font-family: 'Space Mono';
font-size: 13px;
font-weight: 500;
white-space: nowrap;
}
}
}

View File

@@ -1,230 +0,0 @@
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { Color } from '@signozhq/design-tokens';
import { Select } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import CustomSelect from 'components/NewSelect/CustomSelect';
import TextToolTip from 'components/TextToolTip';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { useGetFieldKeys } from 'hooks/dynamicVariables/useGetFieldKeys';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebounce from 'hooks/useDebounce';
import { Info } from '@signozhq/icons';
import { FieldKey } from 'types/api/dynamicVariables/getFieldKeys';
import { isRetryableError as checkIfRetryableError } from 'utils/errorUtils';
import './DynamicVariable.styles.scss';
enum AttributeSource {
ALL_TELEMETRY = 'All telemetry',
LOGS = 'Logs',
METRICS = 'Metrics',
TRACES = 'Traces',
}
function DynamicVariable({
setDynamicVariablesSelectedValue,
dynamicVariablesSelectedValue,
errorAttributeKeyMessage,
}: {
setDynamicVariablesSelectedValue: Dispatch<
SetStateAction<
| {
name: string;
value: string;
}
| undefined
>
>;
dynamicVariablesSelectedValue:
| {
name: string;
value: string;
}
| undefined;
errorAttributeKeyMessage?: string;
}): JSX.Element {
const sources = [
AttributeSource.ALL_TELEMETRY,
AttributeSource.LOGS,
AttributeSource.TRACES,
AttributeSource.METRICS,
];
const [attributeSource, setAttributeSource] = useState<AttributeSource>();
const [attributes, setAttributes] = useState<Record<string, FieldKey[]>>({});
const [selectedAttribute, setSelectedAttribute] = useState<string>();
const [apiSearchText, setApiSearchText] = useState<string>('');
const [errorMessage, setErrorMessage] = useState<string>();
const [isRetryableError, setIsRetryableError] = useState<boolean>(true);
const debouncedApiSearchText = useDebounce(apiSearchText, DEBOUNCE_DELAY);
const [filteredAttributes, setFilteredAttributes] = useState<
Record<string, FieldKey[]>
>({});
useEffect(() => {
if (dynamicVariablesSelectedValue?.name) {
setSelectedAttribute(dynamicVariablesSelectedValue.name);
}
if (dynamicVariablesSelectedValue?.value) {
setAttributeSource(dynamicVariablesSelectedValue.value as AttributeSource);
}
}, [
dynamicVariablesSelectedValue?.name,
dynamicVariablesSelectedValue?.value,
]);
const { data, error, isLoading, refetch } = useGetFieldKeys({
signal:
attributeSource === AttributeSource.ALL_TELEMETRY
? undefined
: (attributeSource?.toLowerCase() as 'traces' | 'logs' | 'metrics'),
name: debouncedApiSearchText,
});
const isComplete = useMemo(() => data?.data?.complete === true, [data]);
useEffect(() => {
if (data) {
const newAttributes = data.data?.keys ?? {};
setAttributes(newAttributes);
setFilteredAttributes(newAttributes);
}
}, [data]);
// Handle error from useGetFieldKeys
useEffect(() => {
if (error) {
// Check if error is retryable (5xx) or not (4xx)
const isRetryable = checkIfRetryableError(error);
setIsRetryableError(isRetryable);
}
}, [error]);
// refetch when attributeSource changes
useEffect(() => {
if (attributeSource) {
refetch();
}
}, [attributeSource, refetch, debouncedApiSearchText]);
// Handle search based on whether we have complete data or not
const handleSearch = useCallback(
(text: string) => {
if (isComplete) {
// If complete is true, do client-side filtering
if (!text) {
setFilteredAttributes(attributes);
return;
}
const filtered: Record<string, FieldKey[]> = {};
Object.keys(attributes).forEach((key) => {
if (key.toLowerCase().includes(text.toLowerCase())) {
filtered[key] = attributes[key];
}
});
setFilteredAttributes(filtered);
} else {
// If complete is false, debounce the API call
setApiSearchText(text);
}
},
[attributes, isComplete],
);
// update setDynamicVariablesSelectedValue with debounce when attribute and source is selected
useEffect(() => {
if (selectedAttribute || attributeSource) {
setDynamicVariablesSelectedValue({
name: selectedAttribute || dynamicVariablesSelectedValue?.name || '',
value:
attributeSource ||
dynamicVariablesSelectedValue?.value ||
AttributeSource.ALL_TELEMETRY,
});
}
}, [
selectedAttribute,
attributeSource,
setDynamicVariablesSelectedValue,
dynamicVariablesSelectedValue?.name,
dynamicVariablesSelectedValue?.value,
]);
const isDarkMode = useIsDarkMode();
const errorText = (error as any)?.message || errorMessage;
return (
<div className="dynamic-variable-container">
<div className="dynamic-variable-config-container">
<CustomSelect
placeholder="Select a field"
options={Object.keys(filteredAttributes).map((key) => ({
label: key,
value: key,
}))}
loading={isLoading}
status={errorText ? 'error' : undefined}
onChange={(value): void => {
setSelectedAttribute(value);
}}
showSearch
errorMessage={errorText as any}
value={selectedAttribute || dynamicVariablesSelectedValue?.name}
onSearch={handleSearch}
onRetry={(): void => {
// reset error message
setErrorMessage(undefined);
setIsRetryableError(true);
refetch();
}}
showRetryButton={isRetryableError}
/>
<Typography className="dynamic-variable-from-text">from</Typography>
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
<TextToolTip
text="By default, this searches across logs, traces, and metrics, which can be slow. Selecting a single source improves performance. Many fields share the same values across different signals (for example, `k8s.pod.name` is identical in logs, traces and metrics) making one source enough. Only use `All telemetry` when you need fields that have different values in different signal types."
useFilledIcon={false}
outlinedIcon={
<Info
size={14}
style={{
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
marginTop: 1,
}}
/>
}
/>
</span>
<Select
placeholder="Source"
defaultValue={AttributeSource.ALL_TELEMETRY}
options={sources.map((source) => ({ label: source, value: source }))}
onChange={(value): void => setAttributeSource(value as AttributeSource)}
value={attributeSource || dynamicVariablesSelectedValue?.value}
/>
</div>
{errorAttributeKeyMessage && (
<div>
<Typography.Text color="warning">
{errorAttributeKeyMessage}
</Typography.Text>
</div>
)}
</div>
);
}
DynamicVariable.defaultProps = {
errorAttributeKeyMessage: '',
};
export default DynamicVariable;

View File

@@ -1,382 +0,0 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { useGetFieldKeys } from 'hooks/dynamicVariables/useGetFieldKeys';
import DynamicVariable from '../DynamicVariable';
// Mock scrollIntoView since it's not available in JSDOM
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Mock dependencies
jest.mock('hooks/dynamicVariables/useGetFieldKeys', () => ({
useGetFieldKeys: jest.fn(),
}));
jest.mock('hooks/useDebounce', () => ({
__esModule: true,
default: (value: any): any => value, // Return the same value without debouncing for testing
}));
describe('DynamicVariable Component', () => {
const mockSetDynamicVariablesSelectedValue = jest.fn();
const ATTRIBUTE_PLACEHOLDER = 'Select a field';
const LOADING_TEXT = 'Refreshing values...';
const DEFAULT_PROPS = {
setDynamicVariablesSelectedValue: mockSetDynamicVariablesSelectedValue,
dynamicVariablesSelectedValue: undefined,
errorAttributeKeyMessage: '',
};
const mockFieldKeysResponse = {
data: {
keys: {
'service.name': [],
'http.status_code': [],
duration: [],
},
complete: true,
},
httpStatusCode: 200,
};
beforeEach(() => {
jest.clearAllMocks();
// Default mock implementation
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: mockFieldKeysResponse,
error: null,
isLoading: false,
refetch: jest.fn(),
});
});
// Helper function to get the attribute select element
const getAttributeSelect = (): HTMLElement =>
screen.getAllByRole('combobox')[0];
// Helper function to get the source select element
const getSourceSelect = (): HTMLElement => screen.getAllByRole('combobox')[1];
it('renders with default state', () => {
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Check for main components
expect(screen.getByText(ATTRIBUTE_PLACEHOLDER)).toBeInTheDocument();
expect(screen.getByText('All telemetry')).toBeInTheDocument();
expect(screen.getByText('from')).toBeInTheDocument();
});
it('uses existing values from dynamicVariablesSelectedValue prop', () => {
const selectedValue = {
name: 'service.name',
value: 'Logs',
};
render(
<DynamicVariable
setDynamicVariablesSelectedValue={mockSetDynamicVariablesSelectedValue}
dynamicVariablesSelectedValue={selectedValue}
/>,
);
// Verify values are set
expect(screen.getByText('service.name')).toBeInTheDocument();
expect(screen.getByText('Logs')).toBeInTheDocument();
});
it('shows loading state when fetching data', () => {
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: null,
error: null,
isLoading: true,
refetch: jest.fn(),
});
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Open the CustomSelect dropdown
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Should show loading state
expect(screen.getByText(LOADING_TEXT)).toBeInTheDocument();
});
it('shows error message when API fails', () => {
const errorMessage = 'Failed to fetch field keys';
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: null,
error: { message: errorMessage },
isLoading: false,
refetch: jest.fn(),
});
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Open the CustomSelect dropdown
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Should show error message
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
it('updates filteredAttributes when data is loaded', async () => {
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Open the CustomSelect dropdown
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Wait for options to appear in the dropdown
await waitFor(() => {
// Looking for option-content elements inside the CustomSelect dropdown
const options = document.querySelectorAll('.option-content');
expect(options.length).toBeGreaterThan(0);
// Check if all expected options are present
let foundServiceName = false;
let foundHttpStatusCode = false;
let foundDuration = false;
options.forEach((option) => {
const text = option.textContent?.trim();
if (text === 'service.name') {
foundServiceName = true;
}
if (text === 'http.status_code') {
foundHttpStatusCode = true;
}
if (text === 'duration') {
foundDuration = true;
}
});
expect(foundServiceName).toBe(true);
expect(foundHttpStatusCode).toBe(true);
expect(foundDuration).toBe(true);
});
});
it('calls setDynamicVariablesSelectedValue when attribute is selected', async () => {
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Open the attribute dropdown
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Wait for options to appear, then click on service.name
await waitFor(() => {
// Need to find the option-item containing service.name
const serviceNameOption = screen.getByText('service.name');
expect(serviceNameOption).not.toBeNull();
expect(serviceNameOption?.textContent).toBe('service.name');
// Click on the option-item that contains service.name
const optionElement = serviceNameOption?.closest('.option-item');
if (optionElement) {
fireEvent.click(optionElement);
}
});
// Check if the setter was called with the correct value
expect(mockSetDynamicVariablesSelectedValue).toHaveBeenCalledWith({
name: 'service.name',
value: 'All telemetry',
});
});
it('calls setDynamicVariablesSelectedValue when source is selected', () => {
const mockRefetch = jest.fn();
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: mockFieldKeysResponse,
error: null,
isLoading: false,
refetch: mockRefetch,
});
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Get the Select component
const select = screen
.getByText('All telemetry')
.closest('div[class*="ant-select"]');
expect(select).toBeInTheDocument();
// Directly call the onChange handler by simulating the Select's onChange
// Find the props.onChange of the Select component and call it directly
fireEvent.mouseDown(select as HTMLElement);
// Use a more specific selector to find the "Logs" option
const optionsContainer = document.querySelector(
'.rc-virtual-list-holder-inner',
);
expect(optionsContainer).not.toBeNull();
// Find the option with Logs text content
const logsOption = Array.from(
optionsContainer?.querySelectorAll('.ant-select-item-option-content') || [],
)
.find((element) => element.textContent === 'Logs')
?.closest('.ant-select-item-option');
expect(logsOption).not.toBeNull();
// Click on it
if (logsOption) {
fireEvent.click(logsOption);
}
// Check if the setter was called with the correct value
expect(mockSetDynamicVariablesSelectedValue).toHaveBeenCalledWith(
expect.objectContaining({
value: 'Logs',
}),
);
});
it('filters attributes locally when complete is true', async () => {
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Open the attribute dropdown
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Mock the filter function behavior
const attributeKeys = Object.keys(mockFieldKeysResponse.data.keys);
// Only "http.status_code" should match the filter
const expectedFilteredKeys = attributeKeys.filter((key) =>
key.includes('http'),
);
// Verify our expected filtering logic
expect(expectedFilteredKeys).toContain('http.status_code');
expect(expectedFilteredKeys).not.toContain('service.name');
expect(expectedFilteredKeys).not.toContain('duration');
// Now verify the component's filtering ability by inputting the search text
const inputElement = screen
.getAllByRole('combobox')[0]
.querySelector('input');
if (inputElement) {
fireEvent.change(inputElement, { target: { value: 'http' } });
}
});
it('triggers API call when complete is false and search text changes', async () => {
const mockRefetch = jest.fn();
// Set up the mock to indicate that data is not complete
// and needs to be fetched from the server
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: {
data: {
keys: {
'http.status_code': [],
},
complete: false, // This indicates server-side filtering is needed
},
httpStatusCode: 200,
},
error: null,
isLoading: false,
refetch: mockRefetch,
});
// Render with Logs as the initial source
render(
<DynamicVariable
{...DEFAULT_PROPS}
dynamicVariablesSelectedValue={{
name: '',
value: 'Logs',
}}
/>,
);
// Clear any initial calls
mockRefetch.mockClear();
// Now test the search functionality
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Find the input element and simulate typing
const inputElement = document.querySelector(
'.ant-select-selection-search-input',
);
if (inputElement) {
// Simulate typing in the search input
fireEvent.change(inputElement, { target: { value: 'http' } });
// Verify that the input has the correct value
expect((inputElement as HTMLInputElement).value).toBe('http');
// Wait for the effect to run and verify refetch was called
await waitFor(
() => {
expect(mockRefetch).toHaveBeenCalled();
},
{ timeout: 3000 },
); // Increase timeout to give more time for the effect to run
}
});
it('triggers refetch when attributeSource changes', async () => {
const mockRefetch = jest.fn();
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: mockFieldKeysResponse,
error: null,
isLoading: false,
refetch: mockRefetch,
});
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Clear any initial calls
mockRefetch.mockClear();
// Find and click on the source select to open dropdown
const sourceSelectElement = getSourceSelect();
fireEvent.mouseDown(sourceSelectElement);
// Find and click on the "Metrics" option
const metricsOption = screen.getByText('Metrics');
fireEvent.click(metricsOption);
// Wait for the effect to run
await waitFor(() => {
// Verify that refetch was called after source selection
expect(mockRefetch).toHaveBeenCalled();
});
});
it('shows retry button when error occurs', () => {
const mockRefetch = jest.fn();
(useGetFieldKeys as jest.Mock).mockReturnValue({
data: null,
error: { message: 'Failed to fetch field keys' },
isLoading: false,
refetch: mockRefetch,
});
render(<DynamicVariable {...DEFAULT_PROPS} />);
// Open the attribute dropdown
const attributeSelectElement = getAttributeSelect();
fireEvent.mouseDown(attributeSelectElement);
// Find and click reload icon (retry button)
const reloadIcon = screen.getByTestId('retry-button');
fireEvent.click(reloadIcon);
// Should trigger refetch
expect(mockRefetch).toHaveBeenCalled();
});
});

View File

@@ -1,420 +0,0 @@
.query-container {
display: flex;
flex-flow: row wrap;
min-width: 0;
gap: 1rem;
margin-bottom: 1rem;
flex-direction: column;
}
.variable-item-container {
display: flex;
flex-direction: column;
border-radius: 3px;
border: 1px solid var(--l1-border);
.all-variables {
display: flex;
padding: 10px 16px;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--l1-border);
.all-variables-btn {
display: flex;
align-items: center;
height: 24px;
padding: 0px;
color: var(--muted-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
}
.all-variables-btn:hover {
background-color: unset !important;
}
}
.variable-item-content {
padding: 12px 16px 20px 16px;
display: flex;
flex-direction: column;
gap: 20px;
.variable-name-section {
flex-direction: column;
gap: 8px;
.typography-variables {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
}
.name-input {
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
padding: 6px 6px 6px 8px;
}
}
.variable-description-section {
flex-direction: column;
gap: 8px;
.typography-variables {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
}
.description-input {
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
padding: 6px 6px 6px 8px;
}
}
.variable-type-section {
justify-content: space-between;
margin-bottom: 0px;
align-items: center;
.variable-type-label-container {
display: flex;
align-items: center;
gap: 8px;
}
.typography-variables {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
}
.variable-type-btn-group {
display: grid;
grid-template-columns: max-content max-content max-content max-content;
height: 32px;
flex-shrink: 0;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
.ant-btn {
width: 114px;
height: 32px;
flex-shrink: 0;
border-radius: 2px 0px 0px 2px;
width: 100%;
}
.variable-type-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
.sidenav-beta-tag {
margin-left: 4px;
}
div {
display: flex;
align-items: center;
}
}
.variable-type-btn + .variable-type-btn {
border-left: 1px solid var(--l1-border);
}
.selected {
background: var(--l1-border);
}
}
}
.sort-values-section {
justify-content: space-between;
margin-bottom: 0;
.typography-variables {
color: var(--l1-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.typography-sort {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
letter-spacing: -0.06px;
}
.sort-input {
border-radius: 2px;
border: 1px solid var(--l1-border);
.ant-select-selector {
width: 192px;
height: 32px;
padding: 6px 6px 6px 8px;
background: var(--l3-background);
}
}
}
.multiple-values-section {
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0;
.typography-variables {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
width: 339px;
}
}
.all-option-section {
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0;
.typography-variables {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
width: 339px;
}
}
.default-value-section {
display: grid;
grid-template-columns: max-content 1fr;
.typography-variables {
display: block;
}
.default-value-description {
display: block;
color: var(--l2-foreground);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
}
}
.dynamic-variable-section {
justify-content: space-between;
margin-bottom: 0;
.typography-variables {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
width: 339px;
}
}
.variable-textbox-section {
justify-content: space-between;
margin-bottom: 0;
align-items: center;
.typography-variables {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
}
.default-input {
display: flex;
height: 32px;
padding: 6px 6px 6px 8px;
align-items: flex-start;
gap: 4px;
flex: 1 0 0;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l3-background);
width: 342px;
}
}
.variable-custom-section {
margin-bottom: 0px;
.custom-collapse {
width: 100%;
border-radius: 3px 3px 0px 0px;
border: 1px solid var(--l1-border);
.ant-collapse-item {
border-bottom: none;
}
.ant-collapse-header {
height: 38px;
border-radius: 3px 3px 0px 0px;
background: var(--l3-background);
align-items: center;
padding: 12px;
gap: 8px;
.ant-collapse-expand-icon {
padding-inline-end: 0px;
}
.ant-collapse-header-text {
color: var(--bg-robin-400);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
display: flex;
padding: 1px 2px;
align-items: center;
gap: 10px;
border-radius: 2px;
background: color-mix(in srgb, var(--bg-robin-400) 8%, transparent);
}
}
.ant-collapse-content {
border-top: none;
.ant-collapse-content-box {
padding: 0px;
}
.comma-input {
height: 109px;
border: none;
}
}
}
}
.variables-preview-section {
display: flex;
flex-direction: column;
margin-bottom: 0px;
border-radius: 0px 0px 3px 3px;
border: 1px solid var(--l1-border);
height: 108px;
margin-top: -20px;
border-collapse: collapse;
gap: 5px;
flex-flow: column;
.typography-variables {
color: var(--bg-robin-400);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
display: inline-flex;
padding: 1px 2px;
align-items: center;
gap: 10px;
border-radius: 0px 0px 2px 0px;
background: color-mix(in srgb, var(--bg-robin-400) 8%, transparent);
}
.preview-values {
padding: 4.5px 11px;
display: flex;
overflow-y: auto;
flex-flow: wrap;
gap: 8px;
[data-slot='badge'] {
height: 30px;
color: var(--l1-foreground);
font-family: 'Space Mono';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
display: inline-flex;
letter-spacing: -0.07px;
align-items: center;
border-radius: 2px;
border: 1px solid var(--l1-border);
margin-inline-end: 0px;
}
}
}
}
}
.variable-item-footer {
margin-top: 12px;
display: flex;
flex-direction: row-reverse;
.footer-btn-discard {
display: flex;
align-items: center;
border-radius: 2px;
border: 1px solid var(--l1-border);
background: var(--l1-border);
height: 34px;
padding: 4px 8px 4px 10px;
box-shadow: none;
}
.footer-btn-save {
display: flex;
align-items: center;
border-radius: 2px;
border: 1px solid var(--primary-background);
background: var(--primary-background);
width: 123px;
height: 34px;
padding: 4px 8px 4px 10px;
box-shadow: none;
}
}

View File

@@ -1,828 +0,0 @@
import {
fireEvent,
render,
screen,
userEvent,
waitFor,
} from 'tests/test-utils';
import {
IDashboardVariable,
TSortVariableValuesType,
VariableSortTypeArr,
} from 'types/api/dashboard/getAll';
import VariableItem from './VariableItem';
// Mock modules
jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
__esModule: true,
default: jest.fn().mockResolvedValue({
payload: {
variableValues: ['value1', 'value2', 'value3'],
},
}),
}));
jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('test-uuid'),
}));
// Mock functions
const onCancel = jest.fn();
const onSave = jest.fn();
const validateName = jest.fn(() => true);
const validateAttributeKey = jest.fn(() => true);
// Mode constant
const VARIABLE_MODE = 'ADD';
// Common text constants
const TEXT = {
INCLUDE_ALL_VALUES: 'Include an option for ALL values',
ENABLE_MULTI_VALUES: 'Enable multiple values to be checked',
VARIABLE_EXISTS: 'Variable name already exists',
VARIABLE_WHITESPACE: 'Variable name cannot contain whitespaces',
ATTRIBUTE_KEY_EXISTS: 'A variable with this attribute key already exists',
SORT_VALUES: 'Sort Values',
DEFAULT_VALUE: 'Default Value',
ALL_VARIABLES: 'All variables',
DISCARD: 'Discard',
OPTIONS: 'Options',
QUERY: 'Query',
TEXTBOX: 'Textbox',
CUSTOM: 'Custom',
DYNAMIC: 'Dynamic',
};
// Common test constants
const VARIABLE_DEFAULTS = {
sort: VariableSortTypeArr[0] as TSortVariableValuesType,
multiSelect: false,
showALLOption: false,
};
// Common variable properties
const TEST_VAR_NAMES = {
VAR1: 'variable1',
VAR2: 'variable2',
VAR3: 'variable3',
};
const TEST_VAR_IDS = {
VAR1: 'var1',
VAR2: 'var2',
VAR3: 'var3',
};
const TEST_VAR_DESCRIPTIONS = {
VAR1: 'Variable 1',
VAR2: 'Variable 2',
VAR3: 'Variable 3',
};
// Common UI elements
const SAVE_BUTTON_TEXT = 'Save Variable';
const UNIQUE_NAME_PLACEHOLDER = 'Unique name of the variable';
// Basic variable data for testing
const basicVariableData: IDashboardVariable = {
id: TEST_VAR_IDS.VAR1,
name: TEST_VAR_NAMES.VAR1,
description: 'Test Variable 1',
type: 'QUERY',
queryValue: 'SELECT * FROM test',
...VARIABLE_DEFAULTS,
order: 0,
};
// Helper function to render VariableItem with common props
const renderVariableItem = (
variableData: IDashboardVariable = basicVariableData,
existingVariables: Record<string, IDashboardVariable> = {},
validateNameFn = validateName,
validateAttributeKeyFn = validateAttributeKey,
): void => {
render(
<VariableItem
variableData={variableData}
existingVariables={existingVariables}
onCancel={onCancel}
onSave={onSave}
validateName={validateNameFn}
validateAttributeKey={validateAttributeKeyFn}
mode={VARIABLE_MODE}
/>,
);
};
// Helper function to find button by text within its span
const findButtonByText = (text: string): HTMLElement | null => {
const buttons = screen.getAllByRole('button');
return buttons.find((button) => button.textContent?.includes(text)) || null;
};
describe('VariableItem Component', () => {
// Test SQL query patterns
const SQL_PATTERN_DOT = 'SELECT * FROM test WHERE env = {{.variable2}}';
const SQL_PATTERN_DOLLAR = 'SELECT * FROM test WHERE env = $variable2';
const SQL_PATTERN_BRACKET = 'SELECT * FROM test WHERE service = [[variable3]]';
const SQL_PATTERN_BRACES = 'SELECT * FROM test WHERE app = {{variable1}}';
const SQL_PATTERN_NO_VARS = 'SELECT * FROM test WHERE env = "prod"';
const SQL_PATTERN_DOT_VAR1 =
'SELECT * FROM test WHERE service = {{.variable1}}';
// Error message text constant
const CIRCULAR_DEPENDENCY_ERROR = /Cannot save: Circular dependency detected/;
// Test functions and utilities
const createVariable = (
id: string,
name: string,
description: string,
queryValue: string,
order: number,
): IDashboardVariable => ({
id,
name,
description,
type: 'QUERY',
queryValue,
...VARIABLE_DEFAULTS,
order,
});
beforeEach(() => {
jest.clearAllMocks();
});
it('renders without crashing', () => {
renderVariableItem();
expect(screen.getByText(TEXT.ALL_VARIABLES)).toBeInTheDocument();
expect(screen.getByText('Name')).toBeInTheDocument();
expect(screen.getByText('Description')).toBeInTheDocument();
expect(screen.getByText('Variable Type')).toBeInTheDocument();
});
describe('Variable Name Validation', () => {
it('shows error when variable name already exists', () => {
// Set validateName to return false (name exists)
const mockValidateName = jest.fn().mockReturnValue(false);
renderVariableItem({ ...basicVariableData, name: '' }, {}, mockValidateName);
// Enter a name that already exists
const nameInput = screen.getByPlaceholderText(UNIQUE_NAME_PLACEHOLDER);
fireEvent.change(nameInput, { target: { value: 'existingVariable' } });
// Error message should be displayed
expect(screen.getByText(TEXT.VARIABLE_EXISTS)).toBeInTheDocument();
// We won't check for button disabled state as it might be inconsistent in tests
});
it('allows save when current variable name is used', () => {
// Mock validate to return false for all other names but true for own name
const mockValidateName = jest
.fn()
.mockImplementation((name) => name === TEST_VAR_NAMES.VAR1);
renderVariableItem(basicVariableData, {}, mockValidateName);
// Enter the current variable name
const nameInput = screen.getByPlaceholderText(UNIQUE_NAME_PLACEHOLDER);
fireEvent.change(nameInput, { target: { value: TEST_VAR_NAMES.VAR1 } });
// Error should not be visible
expect(screen.queryByText(TEXT.VARIABLE_EXISTS)).not.toBeInTheDocument();
});
it('shows error when variable name contains whitespace', () => {
renderVariableItem({ ...basicVariableData, name: '' });
// Enter a name with whitespace
const nameInput = screen.getByPlaceholderText(UNIQUE_NAME_PLACEHOLDER);
fireEvent.change(nameInput, { target: { value: 'variable name' } });
// Error message should be displayed
expect(screen.getByText(TEXT.VARIABLE_WHITESPACE)).toBeInTheDocument();
// Save button should be disabled
const saveButton = screen.getByRole('button', { name: /save variable/i });
expect(saveButton).toBeDisabled();
});
it('allows variable name without whitespace', () => {
renderVariableItem({ ...basicVariableData, name: '' });
// Enter a valid name without whitespace
const nameInput = screen.getByPlaceholderText(UNIQUE_NAME_PLACEHOLDER);
fireEvent.change(nameInput, { target: { value: 'variable.name' } });
// Error should not be visible
expect(screen.queryByText(TEXT.VARIABLE_WHITESPACE)).not.toBeInTheDocument();
});
it('validates whitespace in auto-generated name for dynamic variables', () => {
// Create a dynamic variable with empty name
const dynamicVariable: IDashboardVariable = {
...basicVariableData,
name: '',
type: 'DYNAMIC',
dynamicVariablesAttribute: 'service name', // Contains whitespace
dynamicVariablesSource: 'All telemetry',
};
renderVariableItem(dynamicVariable);
// Error message should be displayed for auto-generated name
expect(screen.getByText(TEXT.VARIABLE_WHITESPACE)).toBeInTheDocument();
});
});
describe('Dynamic Variable Attribute Key Validation', () => {
it('shows error when attribute key already exists', async () => {
// Mock validateAttributeKey to return false (attribute key exists)
const mockValidateAttributeKey = jest.fn().mockReturnValue(false);
// Create a dynamic variable
const dynamicVariable: IDashboardVariable = {
...basicVariableData,
name: 'test-variable',
type: 'DYNAMIC',
dynamicVariablesAttribute: 'service.name',
dynamicVariablesSource: 'All telemetry',
};
renderVariableItem(
dynamicVariable,
{},
validateName,
mockValidateAttributeKey,
);
// Switch to Dynamic type to trigger the validation
const dynamicButton = findButtonByText(TEXT.DYNAMIC);
if (dynamicButton) {
fireEvent.click(dynamicButton);
}
// Error message should be displayed
await waitFor(() => {
expect(screen.getByText(TEXT.ATTRIBUTE_KEY_EXISTS)).toBeInTheDocument();
});
// Save button should be disabled
const saveButton = screen.getByRole('button', { name: /save variable/i });
expect(saveButton).toBeDisabled();
});
it('allows saving when attribute key is unique', async () => {
// Mock validateAttributeKey to return true (attribute key is unique)
const mockValidateAttributeKey = jest.fn().mockReturnValue(true);
// Create a dynamic variable
const dynamicVariable: IDashboardVariable = {
...basicVariableData,
name: 'test-variable',
type: 'DYNAMIC',
dynamicVariablesAttribute: 'service.name',
dynamicVariablesSource: 'All telemetry',
};
renderVariableItem(
dynamicVariable,
{},
validateName,
mockValidateAttributeKey,
);
// Switch to Dynamic type
const dynamicButton = findButtonByText(TEXT.DYNAMIC);
if (dynamicButton) {
fireEvent.click(dynamicButton);
}
// Error should not be visible
await waitFor(() => {
expect(
screen.queryByText(TEXT.ATTRIBUTE_KEY_EXISTS),
).not.toBeInTheDocument();
});
// Save button should not be disabled due to attribute key error
const saveButton = screen.getByRole('button', { name: /save variable/i });
expect(saveButton).not.toBeDisabled();
});
it('allows same attribute key for current variable being edited', async () => {
// Mock validateAttributeKey to return true for same variable
const mockValidateAttributeKey = jest.fn().mockImplementation(
(attributeKey, currentVariableId) =>
// Allow if it's the same variable ID
currentVariableId === TEST_VAR_IDS.VAR1,
);
// Create a dynamic variable
const dynamicVariable: IDashboardVariable = {
...basicVariableData,
id: TEST_VAR_IDS.VAR1,
name: 'test-variable',
type: 'DYNAMIC',
dynamicVariablesAttribute: 'service.name',
dynamicVariablesSource: 'All telemetry',
};
renderVariableItem(
dynamicVariable,
{},
validateName,
mockValidateAttributeKey,
);
// Error should not be visible
await waitFor(() => {
expect(
screen.queryByText(TEXT.ATTRIBUTE_KEY_EXISTS),
).not.toBeInTheDocument();
});
});
it('does not validate attribute key for non-dynamic variables', async () => {
// Mock validateAttributeKey to return false (would show error for dynamic)
const mockValidateAttributeKey = jest.fn().mockReturnValue(false);
// Create a non-dynamic variable
const queryVariable: IDashboardVariable = {
...basicVariableData,
name: 'test-variable',
type: 'QUERY',
};
renderVariableItem(
queryVariable,
{},
validateName,
mockValidateAttributeKey,
);
// No error should be displayed for query variables
expect(
screen.queryByText(TEXT.ATTRIBUTE_KEY_EXISTS),
).not.toBeInTheDocument();
// validateAttributeKey should not be called for non-dynamic variables
expect(mockValidateAttributeKey).not.toHaveBeenCalled();
});
});
describe('Variable Type Switching', () => {
it('switches to CUSTOM variable type correctly', () => {
renderVariableItem();
// Find the Query button
const queryButton = findButtonByText(TEXT.QUERY);
expect(queryButton).toBeInTheDocument();
expect(queryButton).toHaveClass('selected');
// Find and click Custom button
const customButton = findButtonByText(TEXT.CUSTOM);
expect(customButton).toBeInTheDocument();
if (customButton) {
fireEvent.click(customButton);
}
// Custom button should now be selected
expect(customButton).toHaveClass('selected');
expect(queryButton).not.toHaveClass('selected');
// Custom options input should appear
expect(screen.getByText(TEXT.OPTIONS)).toBeInTheDocument();
});
it('switches to TEXTBOX variable type correctly', () => {
renderVariableItem();
// Find and click Textbox button
const textboxButton = findButtonByText(TEXT.TEXTBOX);
expect(textboxButton).toBeInTheDocument();
if (textboxButton) {
fireEvent.click(textboxButton);
}
// Textbox button should now be selected
expect(textboxButton).toHaveClass('selected');
// Default Value input should appear
expect(screen.getByText(TEXT.DEFAULT_VALUE)).toBeInTheDocument();
expect(
screen.getByPlaceholderText('Enter a default value (if any)...'),
).toBeInTheDocument();
});
});
describe('MultiSelect and ALL Option', () => {
it('enables ALL option only when multiSelect is enabled', async () => {
renderVariableItem();
// Initially, ALL option should not be visible
expect(screen.queryByText(TEXT.INCLUDE_ALL_VALUES)).not.toBeInTheDocument();
// Enable multiple values
const multipleValuesSwitch = screen
.getByText(TEXT.ENABLE_MULTI_VALUES)
.closest('.multiple-values-section')
?.querySelector('button');
expect(multipleValuesSwitch).toBeInTheDocument();
if (multipleValuesSwitch) {
fireEvent.click(multipleValuesSwitch);
}
// Now ALL option should be visible
await waitFor(() => {
expect(screen.getByText(TEXT.INCLUDE_ALL_VALUES)).toBeInTheDocument();
});
// Disable multiple values
if (multipleValuesSwitch) {
fireEvent.click(multipleValuesSwitch);
}
// ALL option should be hidden again
await waitFor(() => {
expect(screen.queryByText(TEXT.INCLUDE_ALL_VALUES)).not.toBeInTheDocument();
});
});
it('disables ALL option when multiSelect is disabled', async () => {
// Create variable with multiSelect and showALLOption both enabled
const variable: IDashboardVariable = {
...basicVariableData,
multiSelect: true,
showALLOption: true,
};
renderVariableItem(variable);
// ALL option should be visible initially
expect(screen.getByText(TEXT.INCLUDE_ALL_VALUES)).toBeInTheDocument();
// Disable multiple values
const multipleValuesSwitch = screen
.getByText(TEXT.ENABLE_MULTI_VALUES)
.closest('.multiple-values-section')
?.querySelector('button');
if (multipleValuesSwitch) {
fireEvent.click(multipleValuesSwitch);
}
// ALL option should be hidden
await waitFor(() => {
expect(screen.queryByText(TEXT.INCLUDE_ALL_VALUES)).not.toBeInTheDocument();
});
// Check that when saving, showALLOption is set to false
const saveButton = screen.getByText(SAVE_BUTTON_TEXT);
fireEvent.click(saveButton);
expect(onSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
multiSelect: false,
showALLOption: false,
}),
expect.anything(), // widgetIds prop
);
});
});
describe('Cancel and Navigation', () => {
it('calls onCancel when clicking All Variables button', () => {
renderVariableItem();
// Click All variables button
const allVariablesButton = screen.getByText(TEXT.ALL_VARIABLES);
fireEvent.click(allVariablesButton);
expect(onCancel).toHaveBeenCalledTimes(1);
});
it('calls onCancel when clicking Discard button', () => {
renderVariableItem();
// Click Discard button
const discardButton = screen.getByText(TEXT.DISCARD);
fireEvent.click(discardButton);
expect(onCancel).toHaveBeenCalledTimes(1);
});
});
describe('Cyclic Dependency Detection', () => {
// Common function to render the component with variables and click save
const renderAndSave = async (
variableData: IDashboardVariable,
existingVariables: Record<string, IDashboardVariable>,
): Promise<void> => {
renderVariableItem(variableData, existingVariables);
// Fill in the variable name if it's not already populated
const nameInput = screen.getByPlaceholderText(UNIQUE_NAME_PLACEHOLDER);
if (nameInput.getAttribute('value') === '') {
fireEvent.change(nameInput, { target: { value: variableData.name || '' } });
}
// Click save button to trigger the dependency check
const saveButton = screen.getByText(SAVE_BUTTON_TEXT);
fireEvent.click(saveButton);
};
// Common expectations for finding circular dependency error
const expectCircularDependencyError = async (): Promise<void> => {
await waitFor(() => {
expect(screen.getByText(CIRCULAR_DEPENDENCY_ERROR)).toBeInTheDocument();
expect(onSave).not.toHaveBeenCalled();
});
};
// Test for cyclic dependency detection
it('detects circular dependency and shows error message', async () => {
// Create variables with circular dependency
const variable1 = createVariable(
TEST_VAR_IDS.VAR1,
TEST_VAR_NAMES.VAR1,
TEST_VAR_DESCRIPTIONS.VAR1,
SQL_PATTERN_DOT,
0,
);
const variable2 = createVariable(
TEST_VAR_IDS.VAR2,
TEST_VAR_NAMES.VAR2,
TEST_VAR_DESCRIPTIONS.VAR2,
SQL_PATTERN_DOT_VAR1,
1,
);
const existingVariables = {
[TEST_VAR_IDS.VAR2]: variable2,
};
await renderAndSave(variable1, existingVariables);
await expectCircularDependencyError();
});
// Test for saving with no circular dependency
it('allows saving when no circular dependency exists', async () => {
// Create variables without circular dependency
const variable1 = createVariable(
TEST_VAR_IDS.VAR1,
TEST_VAR_NAMES.VAR1,
TEST_VAR_DESCRIPTIONS.VAR1,
SQL_PATTERN_NO_VARS,
0,
);
const variable2 = createVariable(
TEST_VAR_IDS.VAR2,
TEST_VAR_NAMES.VAR2,
TEST_VAR_DESCRIPTIONS.VAR2,
SQL_PATTERN_DOT_VAR1,
1,
);
const existingVariables = {
[TEST_VAR_IDS.VAR2]: variable2,
};
await renderAndSave(variable1, existingVariables);
// Verify the onSave function was called
await waitFor(() => {
expect(onSave).toHaveBeenCalled();
});
});
// Test with multiple variable formats in query
it('detects circular dependency with different variable formats', async () => {
// Create variables with circular dependency using different formats
const variable1 = createVariable(
TEST_VAR_IDS.VAR1,
TEST_VAR_NAMES.VAR1,
TEST_VAR_DESCRIPTIONS.VAR1,
SQL_PATTERN_DOLLAR,
0,
);
const variable2 = createVariable(
TEST_VAR_IDS.VAR2,
TEST_VAR_NAMES.VAR2,
TEST_VAR_DESCRIPTIONS.VAR2,
SQL_PATTERN_BRACKET,
1,
);
const variable3 = createVariable(
TEST_VAR_IDS.VAR3,
TEST_VAR_NAMES.VAR3,
TEST_VAR_DESCRIPTIONS.VAR3,
SQL_PATTERN_BRACES,
2,
);
const existingVariables = {
[TEST_VAR_IDS.VAR2]: variable2,
[TEST_VAR_IDS.VAR3]: variable3,
};
await renderAndSave(variable1, existingVariables);
await expectCircularDependencyError();
});
});
describe('Textbox Variable Default Value Handling', () => {
it('saves textbox variable with defaultValue and selectedValue set to textboxValue', async () => {
const user = userEvent.setup();
const textboxVariable: IDashboardVariable = {
id: TEST_VAR_IDS.VAR1,
name: TEST_VAR_NAMES.VAR1,
description: 'Test Textbox Variable',
type: 'TEXTBOX',
textboxValue: 'my-default-value',
...VARIABLE_DEFAULTS,
order: 0,
};
renderVariableItem(textboxVariable);
// Click save button
const saveButton = screen.getByText(SAVE_BUTTON_TEXT);
await user.click(saveButton);
// Verify that onSave was called with defaultValue and selectedValue equal to textboxValue
expect(onSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
type: 'TEXTBOX',
textboxValue: 'my-default-value',
defaultValue: 'my-default-value',
selectedValue: 'my-default-value',
}),
expect.anything(),
);
});
it('saves textbox variable with empty values when textboxValue is empty', async () => {
const user = userEvent.setup();
const textboxVariable: IDashboardVariable = {
id: TEST_VAR_IDS.VAR1,
name: TEST_VAR_NAMES.VAR1,
description: 'Test Textbox Variable',
type: 'TEXTBOX',
textboxValue: '',
...VARIABLE_DEFAULTS,
order: 0,
};
renderVariableItem(textboxVariable);
// Click save button
const saveButton = screen.getByText(SAVE_BUTTON_TEXT);
await user.click(saveButton);
// Verify that onSave was called with empty defaultValue and selectedValue
expect(onSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
type: 'TEXTBOX',
textboxValue: '',
defaultValue: '',
selectedValue: '',
}),
expect.anything(),
);
});
it('updates textbox defaultValue and selectedValue when user changes textboxValue input', async () => {
const user = userEvent.setup();
const textboxVariable: IDashboardVariable = {
id: TEST_VAR_IDS.VAR1,
name: TEST_VAR_NAMES.VAR1,
description: 'Test Textbox Variable',
type: 'TEXTBOX',
textboxValue: 'initial-value',
...VARIABLE_DEFAULTS,
order: 0,
};
renderVariableItem(textboxVariable);
// Change the textbox value
const textboxInput = screen.getByPlaceholderText(
'Enter a default value (if any)...',
);
await user.clear(textboxInput);
await user.type(textboxInput, 'updated-value');
// Click save button
const saveButton = screen.getByText(SAVE_BUTTON_TEXT);
await user.click(saveButton);
// Verify that onSave was called with the updated defaultValue and selectedValue
expect(onSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
type: 'TEXTBOX',
textboxValue: 'updated-value',
defaultValue: 'updated-value',
selectedValue: 'updated-value',
}),
expect.anything(),
);
});
it('non-textbox variables use variableDefaultValue instead of textboxValue', async () => {
const user = userEvent.setup();
const queryVariable: IDashboardVariable = {
id: TEST_VAR_IDS.VAR1,
name: TEST_VAR_NAMES.VAR1,
description: 'Test Query Variable',
type: 'QUERY',
queryValue: 'SELECT * FROM test',
textboxValue: 'should-not-be-used',
defaultValue: 'query-default-value',
...VARIABLE_DEFAULTS,
order: 0,
};
renderVariableItem(queryVariable);
// Click save button
const saveButton = screen.getByText(SAVE_BUTTON_TEXT);
await user.click(saveButton);
// Verify that onSave was called with defaultValue not being textboxValue
expect(onSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
type: 'QUERY',
defaultValue: 'query-default-value',
}),
expect.anything(),
);
// Verify that defaultValue is NOT the textboxValue
const savedVariable = onSave.mock.calls[0][1];
expect(savedVariable.defaultValue).not.toBe('should-not-be-used');
});
it('switching to textbox type sets defaultValue and selectedValue correctly on save', async () => {
const user = userEvent.setup();
// Start with a QUERY variable
const queryVariable: IDashboardVariable = {
id: TEST_VAR_IDS.VAR1,
name: TEST_VAR_NAMES.VAR1,
description: 'Test Variable',
type: 'QUERY',
queryValue: 'SELECT * FROM test',
...VARIABLE_DEFAULTS,
order: 0,
};
renderVariableItem(queryVariable);
// Switch to TEXTBOX type
const textboxButton = findButtonByText(TEXT.TEXTBOX);
expect(textboxButton).toBeInTheDocument();
if (textboxButton) {
await user.click(textboxButton);
}
// Enter a default value in the textbox input
const textboxInput = screen.getByPlaceholderText(
'Enter a default value (if any)...',
);
await user.type(textboxInput, 'new-textbox-default');
// Click save button
const saveButton = screen.getByText(SAVE_BUTTON_TEXT);
await user.click(saveButton);
// Verify that onSave was called with type TEXTBOX and correct defaultValue and selectedValue
expect(onSave).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
type: 'TEXTBOX',
textboxValue: 'new-textbox-default',
defaultValue: 'new-textbox-default',
selectedValue: 'new-textbox-default',
}),
expect.anything(),
);
});
});
});

View File

@@ -1,851 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { orange } from '@ant-design/colors';
import { Color } from '@signozhq/design-tokens';
import { Button, Collapse, Input, Select } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import cx from 'classnames';
import Editor from 'components/Editor';
import { CustomSelect } from 'components/NewSelect';
import TextToolTip from 'components/TextToolTip';
import { PANEL_GROUP_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useWidgetsByDynamicVariableId } from 'hooks/dashboard/useWidgetsByDynamicVariableId';
import { getWidgetsHavingDynamicVariableAttribute } from 'hooks/dashboard/utils';
import { useGetFieldValues } from 'hooks/dynamicVariables/useGetFieldValues';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { commaValuesParser } from 'lib/dashboardVariables/customCommaValuesParser';
import sortValues from 'lib/dashboardVariables/sortVariableValues';
import { isEmpty, map } from 'lodash-es';
import {
ArrowLeft,
Check,
ClipboardType,
DatabaseZap,
Info,
LayoutList,
Pyramid,
X,
} from '@signozhq/icons';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { AppState } from 'store/reducers';
import {
IDashboardVariable,
TSortVariableValuesType,
TVariableQueryType,
VariableSortTypeArr,
Widgets,
} from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid';
import {
buildDependencies,
buildDependencyGraph,
} from '../../../DashboardVariablesSelection/util';
import { variablePropsToPayloadVariables } from '../../../utils';
import { TVariableMode } from '../types';
import DynamicVariable from './DynamicVariable/DynamicVariable';
import { LabelContainer, VariableItemRow } from './styles';
import { WidgetSelector } from './WidgetSelector';
import './VariableItem.styles.scss';
const { Option } = Select;
interface VariableItemProps {
variableData: IDashboardVariable;
existingVariables: Record<string, IDashboardVariable>;
onCancel: () => void;
onSave: (
mode: TVariableMode,
variableData: IDashboardVariable,
widgetIds?: string[],
) => void;
validateName: (arg0: string) => boolean;
validateAttributeKey: (
attributeKey: string,
currentVariableId?: string,
) => boolean;
mode: TVariableMode;
}
function VariableItem({
variableData,
existingVariables,
onCancel,
onSave,
validateName,
validateAttributeKey,
mode,
}: VariableItemProps): JSX.Element {
const [variableName, setVariableName] = useState<string>(
variableData.name || '',
);
const [hasUserManuallyChangedName, setHasUserManuallyChangedName] =
useState<boolean>(false);
const [variableDescription, setVariableDescription] = useState<string>(
variableData.description || '',
);
const [queryType, setQueryType] = useState<TVariableQueryType>(
variableData.type || 'DYNAMIC',
);
const [variableQueryValue, setVariableQueryValue] = useState<string>(
variableData.queryValue || '',
);
const [variableCustomValue, setVariableCustomValue] = useState<string>(
variableData.customValue || '',
);
const [variableTextboxValue, setVariableTextboxValue] = useState<string>(
variableData.textboxValue || '',
);
const [variableSortType, setVariableSortType] =
useState<TSortVariableValuesType>(
variableData.sort || VariableSortTypeArr[0],
);
const [variableMultiSelect, setVariableMultiSelect] = useState<boolean>(
variableData.multiSelect || false,
);
const [variableShowALLOption, setVariableShowALLOption] = useState<boolean>(
variableData.showALLOption || false,
);
const [previewValues, setPreviewValues] = useState<string[]>([]);
const [variableDefaultValue, setVariableDefaultValue] = useState<string>(
(variableData.defaultValue as string) || '',
);
const isDarkMode = useIsDarkMode();
const [dynamicVariablesSelectedValue, setDynamicVariablesSelectedValue] =
useState<{ name: string; value: string }>();
// Error messages
const [errorName, setErrorName] = useState<boolean>(false);
const [errorNameMessage, setErrorNameMessage] = useState<string>('');
const [errorAttributeKey, setErrorAttributeKey] = useState<boolean>(false);
const [errorAttributeKeyMessage, setErrorAttributeKeyMessage] =
useState<string>('');
const [errorPreview, setErrorPreview] = useState<string | null>(null);
useEffect(() => {
if (
variableData.dynamicVariablesAttribute &&
variableData.dynamicVariablesSource
) {
setDynamicVariablesSelectedValue({
name: variableData.dynamicVariablesAttribute,
value: variableData.dynamicVariablesSource,
});
}
}, [
variableData.dynamicVariablesAttribute,
variableData.dynamicVariablesSource,
]);
// Validate attribute key uniqueness for dynamic variables
useEffect(() => {
if (queryType === 'DYNAMIC' && dynamicVariablesSelectedValue?.name) {
if (
!validateAttributeKey(dynamicVariablesSelectedValue.name, variableData.id)
) {
setErrorAttributeKey(true);
setErrorAttributeKeyMessage(
'A variable with this attribute key already exists',
);
} else {
setErrorAttributeKey(false);
setErrorAttributeKeyMessage('');
}
} else {
setErrorAttributeKey(false);
setErrorAttributeKeyMessage('');
}
}, [
queryType,
dynamicVariablesSelectedValue?.name,
validateAttributeKey,
variableData.id,
]);
// Auto-set variable name to selected attribute name in creation mode when user hasn't manually changed it
useEffect(() => {
if (
mode === 'ADD' && // Only in creation mode
queryType === 'DYNAMIC' && // Only for dynamic variables
dynamicVariablesSelectedValue?.name && // Attribute is selected
!hasUserManuallyChangedName // User hasn't manually changed the name
) {
const newName = dynamicVariablesSelectedValue.name;
setVariableName(newName);
// Trigger validation for the auto-set name
if (/\s/.test(newName)) {
setErrorName(true);
setErrorNameMessage('Variable name cannot contain whitespaces');
} else if (!validateName(newName)) {
setErrorName(true);
setErrorNameMessage('Variable name already exists');
} else {
setErrorName(false);
setErrorNameMessage('');
}
}
}, [
mode,
queryType,
dynamicVariablesSelectedValue?.name,
hasUserManuallyChangedName,
validateName,
]);
const REQUIRED_NAME_MESSAGE = 'Variable name is required';
// Initialize error state for empty name
useEffect(() => {
if (!variableName.trim()) {
setErrorName(true);
}
}, [variableName]);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { data: fieldValues } = useGetFieldValues({
signal:
dynamicVariablesSelectedValue?.value === 'All telemetry'
? undefined
: (dynamicVariablesSelectedValue?.value?.toLowerCase() as
| 'traces'
| 'logs'
| 'metrics'),
name: dynamicVariablesSelectedValue?.name || '',
enabled:
!!dynamicVariablesSelectedValue?.name &&
!!dynamicVariablesSelectedValue?.value,
startUnixMilli: minTime,
endUnixMilli: maxTime,
});
const [selectedWidgets, setSelectedWidgets] = useState<string[]>([]);
const { dashboardData } = useDashboardStore();
const widgetsByDynamicVariableId = useWidgetsByDynamicVariableId();
useEffect(() => {
if (variableData?.id && variableData.id in widgetsByDynamicVariableId) {
setSelectedWidgets(widgetsByDynamicVariableId[variableData.id] || []);
} else if (dynamicVariablesSelectedValue?.name) {
const widgets = getWidgetsHavingDynamicVariableAttribute(
dynamicVariablesSelectedValue?.name,
(dashboardData?.data?.widgets?.filter(
(widget) => widget.panelTypes !== PANEL_GROUP_TYPES.ROW,
) || []) as Widgets[],
variableData.name,
);
setSelectedWidgets(widgets || []);
}
}, [
dynamicVariablesSelectedValue?.name,
dashboardData,
variableData.id,
variableData.name,
widgetsByDynamicVariableId,
]);
useEffect(() => {
if (queryType === 'CUSTOM') {
setPreviewValues(
sortValues(
commaValuesParser(variableCustomValue),
variableSortType,
) as never,
);
}
if (queryType === 'QUERY') {
setPreviewValues((prev) => sortValues(prev, variableSortType) as never);
}
}, [
queryType,
variableCustomValue,
variableData.customValue,
variableData.type,
variableSortType,
]);
useEffect(() => {
if (
queryType === 'DYNAMIC' &&
fieldValues &&
dynamicVariablesSelectedValue?.name &&
dynamicVariablesSelectedValue?.value
) {
setPreviewValues(
sortValues(
fieldValues.data?.normalizedValues || [],
variableSortType,
) as never,
);
}
}, [
fieldValues,
variableSortType,
queryType,
dynamicVariablesSelectedValue?.name,
dynamicVariablesSelectedValue?.value,
]);
const variableValue = useMemo(() => {
if (queryType === 'TEXTBOX') {
return variableTextboxValue;
}
if (variableMultiSelect) {
let value = variableData.selectedValue;
if (isEmpty(value)) {
if (variableData.showALLOption) {
if (variableDefaultValue) {
value = variableDefaultValue;
} else {
value = previewValues;
}
} else if (variableDefaultValue) {
value = variableDefaultValue;
} else {
value = previewValues?.[0];
}
}
return value;
}
if (isEmpty(variableData.selectedValue)) {
if (variableDefaultValue) {
return variableDefaultValue;
}
return previewValues?.[0]?.toString();
}
return variableData.selectedValue || variableDefaultValue;
}, [
variableMultiSelect,
variableData.selectedValue,
variableData.showALLOption,
variableDefaultValue,
variableTextboxValue,
queryType,
previewValues,
]);
const handleSave = (): void => {
// Check for cyclic dependencies
const newVariable = {
name: variableName,
description: variableDescription,
type: queryType,
queryValue: variableQueryValue,
customValue: variableCustomValue,
textboxValue: variableTextboxValue,
multiSelect: variableMultiSelect,
showALLOption: queryType === 'DYNAMIC' ? true : variableShowALLOption,
sort: variableSortType,
// the reason we need to do this is because defaultValues are treated differently in case of textbox type
// They are the exact same and not like the other types where defaultValue is a separate field
defaultValue:
queryType === 'TEXTBOX' ? variableTextboxValue : variableDefaultValue,
modificationUUID: generateUUID(),
id: variableData.id || generateUUID(),
order: variableData.order,
...(queryType === 'DYNAMIC' && {
dynamicVariablesAttribute: dynamicVariablesSelectedValue?.name,
dynamicVariablesSource: dynamicVariablesSelectedValue?.value,
}),
selectedValue: variableValue,
allSelected: variableData.allSelected,
};
const allVariables = [...Object.values(existingVariables), newVariable];
const dependencies = buildDependencies(allVariables);
const { hasCycle, cycleNodes } = buildDependencyGraph(dependencies);
if (hasCycle) {
setErrorPreview(
`Cannot save: Circular dependency detected between variables: ${cycleNodes?.join(
' → ',
)}`,
);
return;
}
onSave(mode, newVariable, selectedWidgets);
};
// Fetches the preview values for the SQL variable query
const handleQueryResult = (response: any): void => {
if (response?.payload?.variableValues) {
setPreviewValues(
sortValues(
response.payload?.variableValues || [],
variableSortType,
) as never,
);
} else {
setPreviewValues([]);
}
};
const { isFetching: previewLoading, refetch: runQuery } = useQuery(
[REACT_QUERY_KEY.DASHBOARD_BY_ID, variableData.name, variableName],
{
enabled: false,
queryFn: () =>
dashboardVariablesQuery({
query: variableQueryValue || '',
variables: variablePropsToPayloadVariables(existingVariables),
}),
refetchOnWindowFocus: false,
onSuccess: (response) => {
setErrorPreview(null);
handleQueryResult(response);
},
onError: (error: {
details: {
error: string;
};
}) => {
const { details } = error;
if (details.error) {
let message = details.error;
if ((details.error ?? '').toString().includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
}
setErrorPreview(message);
}
},
},
);
const handleTestRunQuery = useCallback(() => {
runQuery();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<div className="variable-item-container">
<div className="all-variables">
<Button
type="text"
className="all-variables-btn"
icon={<ArrowLeft size={14} />}
onClick={onCancel}
>
All variables
</Button>
</div>
<div className="variable-item-content">
<VariableItemRow className="variable-name-section">
<LabelContainer>
<Typography className="typography-variables">Name</Typography>
</LabelContainer>
<div>
<Input
placeholder="Unique name of the variable"
value={variableName}
className="name-input"
onChange={({ target: { value } }): void => {
setVariableName(value);
setHasUserManuallyChangedName(true); // Mark that user has manually changed the name
// Check for empty name
if (!value.trim()) {
setErrorName(true);
setErrorNameMessage(REQUIRED_NAME_MESSAGE);
}
// Check for whitespace in name
else if (/\s/.test(value)) {
setErrorName(true);
setErrorNameMessage('Variable name cannot contain whitespaces');
}
// Check for duplicate name
else if (!validateName(value) && value !== variableData.name) {
setErrorName(true);
setErrorNameMessage('Variable name already exists');
}
// No errors
else {
setErrorName(false);
setErrorNameMessage('');
}
}}
/>
<div>
<Typography.Text color="warning">{errorNameMessage}</Typography.Text>
</div>
</div>
</VariableItemRow>
<VariableItemRow className="variable-description-section">
<LabelContainer>
<Typography className="typography-variables">Description</Typography>
</LabelContainer>
<Input.TextArea
value={variableDescription}
placeholder="Enter a description for the variable"
className="description-input"
rows={3}
onChange={(e): void => setVariableDescription(e.target.value)}
/>
</VariableItemRow>
<VariableItemRow className="variable-type-section">
<LabelContainer className="variable-type-label-container">
<Typography className="typography-variables">Variable Type</Typography>
<TextToolTip
text="Learn more about supported variable types"
url="https://signoz.io/docs/userguide/manage-variables/#supported-variable-types"
urlText="here"
useFilledIcon={false}
outlinedIcon={
<Info
size={14}
style={{
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
}}
/>
}
/>
</LabelContainer>
<div className="variable-type-btn-group">
<Button
type="text"
icon={<Pyramid size={14} />}
className={cx(
'variable-type-btn',
queryType === 'DYNAMIC' ? 'selected' : '',
)}
onClick={(): void => {
setQueryType('DYNAMIC');
setPreviewValues([]);
// Reset manual change flag if no name is entered
if (!variableName.trim()) {
setHasUserManuallyChangedName(false);
}
}}
>
Dynamic
<Badge color="robin" className="sidenav-beta-tag">
Beta
</Badge>
</Button>
<Button
type="text"
icon={<ClipboardType size={14} />}
className={cx(
'variable-type-btn',
queryType === 'TEXTBOX' ? 'selected' : '',
)}
onClick={(): void => {
setQueryType('TEXTBOX');
setPreviewValues([]);
// Reset manual change flag if no name is entered
if (!variableName.trim()) {
setHasUserManuallyChangedName(false);
}
}}
>
Textbox
</Button>
<Button
type="text"
icon={<LayoutList size={14} />}
className={cx(
'variable-type-btn',
queryType === 'CUSTOM' ? 'selected' : '',
)}
onClick={(): void => {
setQueryType('CUSTOM');
setPreviewValues([]);
// Reset manual change flag if no name is entered
if (!variableName.trim()) {
setHasUserManuallyChangedName(false);
}
}}
>
Custom
</Button>
<Button
type="text"
icon={<DatabaseZap size={14} />}
className={cx(
'variable-type-btn',
queryType === 'QUERY' ? 'selected' : '',
)}
onClick={(): void => {
setQueryType('QUERY');
setPreviewValues([]);
// Reset manual change flag if no name is entered
if (!variableName.trim()) {
setHasUserManuallyChangedName(false);
}
}}
>
Query
<Badge color="amber" className="sidenav-beta-tag">
Not Recommended
</Badge>
<div onClick={(e): void => e.stopPropagation()}>
<TextToolTip
text="Learn why we don't recommend"
url="https://signoz.io/docs/userguide/manage-variables/#why-avoid-clickhouse-query-variables"
urlText="here"
useFilledIcon={false}
outlinedIcon={
<Info
size={14}
style={{
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500,
}}
/>
}
/>
</div>
</Button>
</div>
</VariableItemRow>
{queryType === 'DYNAMIC' && (
<div className="variable-dynamic-section">
<DynamicVariable
setDynamicVariablesSelectedValue={setDynamicVariablesSelectedValue}
dynamicVariablesSelectedValue={dynamicVariablesSelectedValue}
errorAttributeKeyMessage={errorAttributeKeyMessage}
/>
</div>
)}
{queryType === 'QUERY' && (
<div className="query-container">
<LabelContainer>
<Typography>Query</Typography>
</LabelContainer>
<div style={{ flex: 1, position: 'relative' }}>
<Editor
language="sql"
value={variableQueryValue}
onChange={(e): void => setVariableQueryValue(e)}
height="240px"
options={{
fontSize: 13,
wordWrap: 'on',
lineNumbers: 'off',
glyphMargin: false,
folding: false,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
minimap: {
enabled: false,
},
}}
/>
<Button
type="primary"
size="small"
onClick={handleTestRunQuery}
style={{
position: 'absolute',
bottom: 0,
}}
loading={previewLoading}
>
Test Run Query
</Button>
</div>
</div>
)}
{queryType === 'CUSTOM' && (
<VariableItemRow className="variable-custom-section">
<Collapse
collapsible="header"
rootClassName="custom-collapse"
defaultActiveKey={['1']}
items={[
{
key: '1',
label: 'Options',
children: (
<Input.TextArea
value={variableCustomValue}
placeholder="Enter options separated by commas."
rootClassName="comma-input"
onChange={(e): void => {
setVariableCustomValue(e.target.value);
setPreviewValues(
sortValues(
commaValuesParser(e.target.value),
variableSortType,
) as never,
);
}}
/>
),
},
]}
/>
</VariableItemRow>
)}
{queryType === 'TEXTBOX' && (
<VariableItemRow className="variable-textbox-section">
<LabelContainer>
<Typography className="typography-variables">Default Value</Typography>
</LabelContainer>
<Input
value={variableTextboxValue}
className="default-input"
onChange={(e): void => {
setVariableTextboxValue(e.target.value);
}}
placeholder="Enter a default value (if any)..."
style={{ width: 400 }}
/>
</VariableItemRow>
)}
{(queryType === 'QUERY' ||
queryType === 'CUSTOM' ||
queryType === 'DYNAMIC') && (
<>
<VariableItemRow className="variables-preview-section">
<LabelContainer style={{ width: '100%' }}>
<Typography className="typography-variables">
Preview of Values
</Typography>
</LabelContainer>
<div className="preview-values">
{errorPreview ? (
<Typography style={{ color: orange[5] }}>{errorPreview}</Typography>
) : (
map(previewValues, (value, idx) => (
<Badge key={`${value}${idx}`} color="vanilla">
{value.toString()}
</Badge>
))
)}
</div>
</VariableItemRow>
<VariableItemRow className="sort-values-section">
<LabelContainer>
<Typography className="typography-variables">Sort Values</Typography>
</LabelContainer>
<Select
defaultActiveFirstOption
defaultValue={VariableSortTypeArr[0]}
value={variableSortType}
onChange={(value: TSortVariableValuesType): void =>
setVariableSortType(value)
}
className="sort-input"
>
<Option value={VariableSortTypeArr[0]}>Disabled</Option>
<Option value={VariableSortTypeArr[1]}>Ascending</Option>
<Option value={VariableSortTypeArr[2]}>Descending</Option>
</Select>
</VariableItemRow>
<VariableItemRow className="multiple-values-section">
<LabelContainer>
<Typography className="typography-variables">
Enable multiple values to be checked
</Typography>
</LabelContainer>
<Switch
value={variableMultiSelect}
onChange={(e): void => {
setVariableMultiSelect(e);
if (!e) {
setVariableShowALLOption(false);
}
}}
/>
</VariableItemRow>
{variableMultiSelect && queryType !== 'DYNAMIC' && (
<VariableItemRow className="all-option-section">
<LabelContainer>
<Typography className="typography-variables">
Include an option for ALL values
</Typography>
</LabelContainer>
<Switch
value={variableShowALLOption}
onChange={(e): void => setVariableShowALLOption(e)}
/>
</VariableItemRow>
)}
<VariableItemRow className="default-value-section">
<LabelContainer>
<Typography className="typography-variables">Default Value</Typography>
<Typography className="default-value-description">
{queryType === 'QUERY'
? 'Click Test Run Query to see the values or add custom value'
: 'Select a value from the preview values or add custom value'}
</Typography>
</LabelContainer>
<CustomSelect
placeholder="Select a default value"
value={variableDefaultValue}
onChange={(value): void => setVariableDefaultValue(value)}
options={previewValues.map((value) => ({
label: value,
value,
}))}
/>
</VariableItemRow>
</>
)}
{queryType === 'DYNAMIC' && (
<VariableItemRow className="dynamic-variable-section">
<LabelContainer>
<Typography className="typography-variables">
Select Panels to apply this variable
</Typography>
</LabelContainer>
<WidgetSelector
selectedWidgets={selectedWidgets}
setSelectedWidgets={setSelectedWidgets}
/>
</VariableItemRow>
)}
</div>
</div>
<div className="variable-item-footer">
<VariableItemRow>
<Button
type="default"
onClick={onCancel}
icon={<X size={14} />}
className="footer-btn-discard"
>
Discard
</Button>
<Button
type="primary"
onClick={handleSave}
disabled={errorName || errorAttributeKey}
icon={<Check size={14} />}
className="footer-btn-save"
>
Save Variable
</Button>
</VariableItemRow>
</div>
</>
);
}
export default VariableItem;

View File

@@ -1,64 +0,0 @@
import React from 'react';
import { CustomMultiSelect } from 'components/NewSelect';
import { PANEL_GROUP_TYPES } from 'constants/queryBuilder';
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { WidgetRow, Widgets } from 'types/api/dashboard/getAll';
export function WidgetSelector({
selectedWidgets,
setSelectedWidgets,
}: {
selectedWidgets: string[];
setSelectedWidgets: (widgets: string[]) => void;
}): JSX.Element {
const { dashboardData } = useDashboardStore();
// Get layout IDs for cross-referencing
const layoutIds = new Set(
(dashboardData?.data?.layout || []).map((item) => item.i),
);
// Filter and deduplicate widgets by ID, keeping only those with layout entries
// and excluding row widgets since they are not panels that can have variables
const widgets = Object.values(
(dashboardData?.data?.widgets || []).reduce(
(acc: Record<string, WidgetRow | Widgets>, widget: WidgetRow | Widgets) => {
if (
widget.id &&
layoutIds.has(widget.id) &&
widget.panelTypes !== PANEL_GROUP_TYPES.ROW
) {
acc[widget.id] = widget;
}
return acc;
},
{},
),
);
// Filter selectedWidgets to only include widgets that are present in the current layout
const validSelectedWidgets = selectedWidgets.filter((widgetId) =>
layoutIds.has(widgetId),
);
// Update selectedWidgets if any invalid widgets were removed
React.useEffect(() => {
if (validSelectedWidgets.length !== selectedWidgets.length) {
setSelectedWidgets(validSelectedWidgets);
}
}, [validSelectedWidgets, selectedWidgets.length, setSelectedWidgets]);
return (
<CustomMultiSelect
placeholder="Select Panels"
options={widgets.map((widget: WidgetRow | Widgets) => ({
label: generateGridTitle(widget.title),
value: widget.id,
}))}
value={validSelectedWidgets}
onChange={(value): void => setSelectedWidgets(value as string[])}
showLabels
/>
);
}

View File

@@ -1,11 +0,0 @@
import { Row } from 'antd';
import styled from 'styled-components';
export const VariableItemRow = styled(Row)`
gap: 1rem;
margin-bottom: 1rem;
`;
export const LabelContainer = styled.div`
width: 200px;
`;

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