mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-22 01:40:32 +01:00
Compare commits
5 Commits
fts-logs
...
refactor/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e13014baba | ||
|
|
bf201710a7 | ||
|
|
a5adc52276 | ||
|
|
5ddcf33811 | ||
|
|
c0fe996e7a |
315
INPUT_MIGRATION_AFFECTED.md
Normal file
315
INPUT_MIGRATION_AFFECTED.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# antd `Input` → `@signozhq/ui/input` Migration — Affected Files & Verification
|
||||
|
||||
## What changed
|
||||
|
||||
For each file listed below, the import was rewritten:
|
||||
|
||||
```diff
|
||||
- import { Input, ...rest } from 'antd';
|
||||
+ import { Input } from '@signozhq/ui/input';
|
||||
+ import { ...rest } from 'antd';
|
||||
```
|
||||
|
||||
Only the plain antd `<Input>` component was migrated. The following are **not** migrated and continue to use antd:
|
||||
|
||||
- `Input.TextArea` / `Input.Password` / `Input.Search` / `Input.Group`
|
||||
- `InputNumber`
|
||||
- `<Input>` with `addonBefore`, `addonAfter`, `allowClear`, `bordered`, `status="error"`, or `size="small|middle|large"` props
|
||||
- `InputRef` (typed refs)
|
||||
|
||||
No JSX usages of `<Input>` were touched — only the import source was swapped. The component contract (`value`, `onChange`, `placeholder`, `disabled`, `type`, `prefix`, `suffix`, etc.) matches between the two libraries for the cases migrated here.
|
||||
|
||||
---
|
||||
|
||||
## Reverted after TypeScript errors — 4 files (kept on antd, marked with TODO)
|
||||
|
||||
These files were initially migrated, then reverted because `@signozhq/ui` `Input` does not expose the props they need. Each one carries a `TODO(@signozhq/ui-input)` comment above the antd import.
|
||||
|
||||
| File | Blocker |
|
||||
|---|---|
|
||||
| `frontend/src/container/MetricsExplorer/Inspect/MetricTimeAggregation.tsx` | Uses `onWheel` on `<Input type="number">` to blur on scroll. Not exposed on `@signozhq/ui/input`. |
|
||||
| `frontend/src/container/NewWidget/RightContainer/ContextLinks/UpdateContextLinks.tsx` | URL `<Input>` uses `spellCheck="false"` (along with `autoCorrect`, `autoCapitalize`). `spellCheck` not exposed. |
|
||||
| `frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/CSVInput.tsx` | Spreads antd `InputProps` (`{...otherProps}`) onto `<Input>`. `size: SizeType` clashes with `@signozhq/ui` Input's numeric `size`. |
|
||||
| `frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/styles.ts` | The `styled(Input)` wrapper is consumed in `Duration/index.tsx` with `addonAfter="ms"`. Not exposed. |
|
||||
|
||||
---
|
||||
|
||||
## Migrated files — 51 total
|
||||
|
||||
Grouped by the route/page that renders them. Visit each route after the migration and confirm the input still:
|
||||
|
||||
1. Renders with the expected layout/spacing
|
||||
2. Accepts keyboard input and reflects controlled `value`
|
||||
3. Fires `onChange` correctly (search filters, form submissions still work)
|
||||
4. Shows placeholder text
|
||||
5. Disables/enables as expected
|
||||
6. Form validation (`Form.Item rules`) still shows errors
|
||||
|
||||
### Alerts — Create / Edit Alert (`/alerts/new`, `/alerts/edit`)
|
||||
|
||||
| File | Where the input shows up |
|
||||
|---|---|
|
||||
| `frontend/src/container/CreateAlertV2/AlertCondition/ThresholdItem.tsx` | Threshold name + threshold value + recovery threshold inputs in the Alert Condition card |
|
||||
| `frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptions.tsx` | Advanced Options collapse inside Evaluation Settings |
|
||||
| `frontend/src/container/CreateAlertV2/EvaluationSettings/EvaluationWindowPopover/EvaluationWindowDetails.tsx` | Evaluation Window popover (numeric value + unit) |
|
||||
| `frontend/src/container/CreateAlertV2/EvaluationSettings/TimeInput/TimeInput.tsx` | HH:MM:SS time input shared in Evaluation Settings |
|
||||
| `frontend/src/container/CreateAlertV2/NotificationSettings/NotificationSettings.tsx` | Re-notification interval input |
|
||||
|
||||
### Alert Channels (`/settings/channels/new`, `/settings/channels/edit/:channelId`)
|
||||
|
||||
| File | Where the input shows up |
|
||||
|---|---|
|
||||
| `frontend/src/container/FormAlertChannels/index.tsx` | Channel name + send-resolved / common fields |
|
||||
| `frontend/src/container/FormAlertChannels/Settings/Email.tsx` | "To" email field of the Email channel form |
|
||||
| `frontend/src/container/FormAlertChannels/Settings/Webhook.tsx` | Webhook URL / username / password fields |
|
||||
|
||||
### Routing Policies (`/settings/channels` → routing policies tab)
|
||||
|
||||
| File | Where the input shows up |
|
||||
|---|---|
|
||||
| `frontend/src/container/RoutingPolicies/RoutingPolicies.tsx` | Search box on the routing policies list |
|
||||
|
||||
### Organization Settings (`/settings/org-settings`)
|
||||
|
||||
| File | Where the input shows up |
|
||||
|---|---|
|
||||
| `frontend/src/container/OrganizationSettings/InviteTeamMembers/index.tsx` | Email + name fields in the "Invite team members" form (used by the invite-members flow) |
|
||||
|
||||
### Members (`/settings/members`)
|
||||
|
||||
_None directly migrated this round_ — already on `@signozhq/ui/input`.
|
||||
|
||||
### Planned Downtime (`/settings/channels` → Planned Downtime, or `/planned-downtime` depending on plan)
|
||||
|
||||
| File | Where the input shows up |
|
||||
|---|---|
|
||||
| `frontend/src/container/PlannedDowntime/PlannedDowntime.tsx` | "Search downtime" text input above the list |
|
||||
|
||||
### Licenses (`/licenses`)
|
||||
|
||||
| File | Where the input shows up |
|
||||
|---|---|
|
||||
| `frontend/src/container/Licenses/ApplyLicenseForm.tsx` | License key text input on the apply-license form |
|
||||
|
||||
### Dashboards — list, detail, widget editor
|
||||
|
||||
| File | Where the input shows up | Route |
|
||||
|---|---|---|
|
||||
| `frontend/src/container/ListOfDashboard/DashboardsList.tsx` | Main "Search by name, description, or tags…" input on the dashboards list | `/dashboard` |
|
||||
| `frontend/src/container/ListOfDashboard/RequestDashboardBtn.tsx` | "Request dashboard" modal input | `/dashboard` |
|
||||
| `frontend/src/container/ListOfDashboard/DashboardTemplates/DashboardTemplatesModal.tsx` | Templates modal search input | `/dashboard` (open templates) |
|
||||
| `frontend/src/container/DashboardContainer/DashboardDescription/index.tsx` | Dashboard description editor name field | `/dashboard/:dashboardId` |
|
||||
| `frontend/src/container/GridCardLayout/GridCardLayout.tsx` | Section / row name inline edit | `/dashboard/:dashboardId` |
|
||||
| `frontend/src/container/DashboardContainer/visualization/components/ChartManager/ChartManager.tsx` | Chart manager search input | `/dashboard/:dashboardId` |
|
||||
| `frontend/src/container/NewWidget/LeftContainer/ExplorerColumnsRenderer.tsx` | Column-config inputs in the widget editor's left panel | `/dashboard/:dashboardId/:widgetId` |
|
||||
|
||||
### Logs (`/logs/logs-explorer`, `/logs/pipelines`)
|
||||
|
||||
| File | Where the input shows up | Route |
|
||||
|---|---|---|
|
||||
| `frontend/src/container/LogsFilters/index.tsx` | Logs filter sidebar search input | `/logs/logs-explorer` |
|
||||
| `frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx` | Query-builder field value inputs in the legacy logs search | `/logs/logs-explorer` |
|
||||
| `frontend/src/container/LogDetailedView/Overview.tsx` | Log details drawer Overview tab inputs (severity / body filters) | `/logs/logs-explorer` (open a log) |
|
||||
| `frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/ProcessorForm.tsx` | Add-new-processor form fields | `/logs/pipelines` |
|
||||
| `frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/JsonFlattening.tsx` | JSON-flattening processor form | `/logs/pipelines` |
|
||||
| `frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/NameInput.tsx` | "Pipeline name" field in the new-pipeline form | `/logs/pipelines` |
|
||||
|
||||
### Traces (`/traces-explorer`, trace details, funnels)
|
||||
|
||||
| File | Where the input shows up | Route |
|
||||
|---|---|---|
|
||||
| `frontend/src/container/Trace/Search/AllTags/Tag/TagKey.tsx` | Tag-key autocomplete input in the legacy trace search | `/trace` |
|
||||
| `frontend/src/container/Trace/TraceGraphFilter/index.tsx` | Trace-graph filter search/value input | `/trace` |
|
||||
| `frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.tsx` | "Search resource attributes" input in the span details drawer | `/traces-explorer` → open span |
|
||||
| `frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.tsx` | Span details drawer → Attributes search | `/traces-explorer` → open span |
|
||||
| `frontend/src/container/SpanDetailsDrawer/Events/Events.tsx` | Span details drawer → Events search | `/traces-explorer` → open span |
|
||||
| `frontend/src/container/TraceWaterfall/AddSpanToFunnelModal/AddSpanToFunnelModal.tsx` | "Add span to funnel" modal name input (legacy waterfall) | `/trace/:id` |
|
||||
| `frontend/src/pages/TraceDetailsV3/SpanDetailsPanel/SpanPercentile/SpanPercentilePanel.tsx` | Percentile filter input in span details v3 | `/trace/:id` (v3) |
|
||||
| `frontend/src/pages/TraceDetailsV3/TraceWaterfall/AddSpanToFunnelModal/AddSpanToFunnelModal.tsx` | "Add span to funnel" modal in v3 waterfall | `/trace/:id` (v3) |
|
||||
| `frontend/src/pages/TracesFunnels/components/SearchBar/SearchBar.tsx` | Search box on the funnels list page | `/traces/funnels` |
|
||||
| `frontend/src/pages/TracesFunnels/components/RenameFunnel/RenameFunnel.tsx` | Rename-funnel modal input | `/traces/funnels` |
|
||||
|
||||
### Saved Views (`/logs/saved-views`, `/traces/saved-views`)
|
||||
|
||||
| File | Where the input shows up |
|
||||
|---|---|
|
||||
| `frontend/src/components/ExplorerCard/SaveViewWithName.tsx` | "Save view with name" inline input on the explorer save flow |
|
||||
| `frontend/src/container/ExplorerOptions/ExplorerOptions.tsx` | "e.g. External http method view" — name input in the Save View modal across explorers |
|
||||
| `frontend/src/pages/SaveView/index.tsx` | Rename saved view input on the saved-views list |
|
||||
|
||||
### Metrics Explorer (`/metrics-explorer`)
|
||||
|
||||
| File | Where the input shows up |
|
||||
|---|---|
|
||||
| `frontend/src/container/MetricsExplorer/MetricDetails/Metadata.tsx` | Metadata edit inputs in metric details |
|
||||
|
||||
### Messaging Queues (`/messaging-queues`)
|
||||
|
||||
| File | Where the input shows up |
|
||||
|---|---|
|
||||
| `frontend/src/pages/MessagingQueues/MQDetails/DropRateView/EvaluationTimeSelector.tsx` | Evaluation time selector inside Drop Rate view |
|
||||
|
||||
### Integrations (`/integrations`)
|
||||
|
||||
| File | Where the input shows up |
|
||||
|---|---|
|
||||
| `frontend/src/container/Integrations/CloudIntegration/AmazonWebServices/RegionForm/RenderConnectionParams.tsx` | Connection-params text fields in AWS cloud integration setup |
|
||||
|
||||
### Ingestion Settings (`/settings/ingestion-settings`)
|
||||
|
||||
| File | Where the input shows up |
|
||||
|---|---|
|
||||
| `frontend/src/container/IngestionSettings/MultiIngestionSettings.tsx` | "Search for ingestion key…" + add/edit ingestion-key name fields. `<InputNumber>` usages in this file still come from antd. |
|
||||
|
||||
### Onboarding (`/get-started/*`, `/onboarding`)
|
||||
|
||||
| File | Where the input shows up |
|
||||
|---|---|
|
||||
| `frontend/src/container/OnboardingContainer/Steps/DataSource/DataSource.tsx` | Custom data source name input |
|
||||
| `frontend/src/container/OnboardingContainer/Steps/EnvironmentDetails/EnvironmentDetails.tsx` | Environment details (env name / app name) inputs |
|
||||
|
||||
### Other shared components (rendered on multiple routes)
|
||||
|
||||
| File | Where it shows up |
|
||||
|---|---|
|
||||
| `frontend/src/components/CustomTimePicker/TimezonePicker.tsx` | Timezone search input inside the global custom time picker — visible on dashboards, logs, traces, alerts header |
|
||||
| `frontend/src/components/InputWithLabel/InputWithLabel.tsx` | Generic labeled-input wrapper — used in invite flows, threshold editing, etc. |
|
||||
| `frontend/src/components/QuickFilters/QuickFiltersSettings/QuickFiltersSettings.tsx` | "Search filters" input inside the Quick Filters settings drawer (logs/traces/infra/metrics quick-filters) |
|
||||
| `frontend/src/components/QuickFilters/FilterRenderers/Checkbox/Checkbox.tsx` | Search box at the top of each checkbox-style quick filter |
|
||||
| `frontend/src/container/QueryTable/Drilldown/BreakoutOptions.tsx` | "Breakout by" attribute search input in the query-table drilldown menu (dashboards, alerts) |
|
||||
| `frontend/src/components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx` | "Search…" input inside the "Add column" picker in the logs format options menu (rendered on `/logs/logs-explorer`). `<InputNumber>` for max-line config still comes from antd. |
|
||||
|
||||
### `styled(Input)` wrappers — `.ts` style files
|
||||
|
||||
These wrap the now-`@signozhq/ui` `Input` with styled-components. CSS selectors target the descendant `<input>` element so behavior should be unchanged, but visually re-verify on the routes below.
|
||||
|
||||
| File | Verification route |
|
||||
|---|---|
|
||||
| `frontend/src/container/Version/styles.ts` | Status page `/status` — confirm input width = 183px |
|
||||
|
||||
> `Trace/Filters/Panel/PanelBody/Duration/styles.ts` was attempted and reverted — see the "Reverted" section at the top.
|
||||
|
||||
---
|
||||
|
||||
## Smoke-test checklist
|
||||
|
||||
A short loop that covers the bulk of what changed:
|
||||
|
||||
1. **Alerts → New alert** (`/alerts/new`) — add a threshold, type a name + value, switch evaluation window unit, expand Advanced Options, set time input, save.
|
||||
2. **Channels → New channel** (`/settings/channels/new`) — pick Email, fill the "to" field; pick Webhook, fill URL.
|
||||
3. **Dashboards** — open a dashboard, edit a panel (`/dashboard/:dashboardId/:widgetId`) → set context-link label/URL/params, edit explorer columns. Then on the list page (`/dashboard`) open the Templates modal and the "Request dashboard" modal.
|
||||
4. **Logs Explorer** (`/logs/logs-explorer`) — type into the sidebar filter search, open a log row → Overview tab.
|
||||
5. **Logs Pipelines** (`/logs/pipelines`) — open "Add new pipeline" and "Add new processor", switch processor type to JSON Flattening / CSV.
|
||||
6. **Traces Explorer** (`/traces-explorer`) — open a span, search inside Attributes + Events tabs.
|
||||
7. **Trace details V3** (`/trace/:id`) — open the percentile panel, open "Add span to funnel".
|
||||
8. **Funnels list** (`/traces/funnels`) — type in the search bar, rename a funnel.
|
||||
9. **Saved Views** (`/logs/saved-views`, `/traces/saved-views`) — rename a view; from an explorer, "Save view as…".
|
||||
10. **Metrics Explorer** (`/metrics-explorer/explorer`) — open Inspect → time aggregation; open metric details → Metadata tab.
|
||||
11. **Integrations → AWS** — open AWS cloud integration → Region form.
|
||||
12. **Onboarding** (`/get-started`) — pick "Custom" data source, fill env/app names.
|
||||
13. **Custom time picker** — open the global time picker anywhere, switch to "Custom" → type in the timezone search.
|
||||
14. **Quick filters** — on `/logs/logs-explorer` or `/traces-explorer`, open the quick-filter settings drawer and search inside.
|
||||
15. **Org settings → Invite members** (`/settings/org-settings`) — open invite flow, fill email + name.
|
||||
16. **Planned downtime** — search the list.
|
||||
17. **Licenses** (`/licenses`) — paste a license key in the input.
|
||||
|
||||
For each: confirm typing, controlled value, placeholder, disabled state, and form-validation error display all behave the same as before.
|
||||
|
||||
---
|
||||
|
||||
## Skipped (still on antd) — 61 files
|
||||
|
||||
After the migration, 61 files still import `Input` from antd. None of them use a plain `<Input>` that can be migrated — each one is kept on antd for one of the reasons below.
|
||||
|
||||
Kept on antd because they use features the `@signozhq/ui` `Input` does not expose. No action required unless these are migrated separately later.
|
||||
|
||||
**Use `Input.TextArea` / `Input.Password` / `Input.Search` / `Input.Group`:**
|
||||
`CreateAlertV2/EvaluationSettings/EvaluationCadence/EvaluationCadence.tsx`,
|
||||
`CreateAlertV2/EvaluationSettings/EvaluationCadence/EvaluationCadenceDetails.tsx`,
|
||||
`CreateAlertV2/NotificationSettings/NotificationMessage.tsx`,
|
||||
`QueryBuilder/components/Formula/Formula.tsx`,
|
||||
`OptionsMenu/AddColumnField/index.tsx`,
|
||||
`LogsSearchFilter/index.tsx`,
|
||||
`RoutingPolicies/RoutingPolicyDetails.tsx`,
|
||||
`DashboardContainer/DashboardSettings/DashboardVariableSettings/VariableItem/VariableItem.tsx`,
|
||||
`DashboardContainer/DashboardSettings/General/index.tsx`,
|
||||
`FormAlertChannels/Settings/MsTeams.tsx`,
|
||||
`MySettings/UserInfo/index.tsx`,
|
||||
`Login/index.tsx`,
|
||||
`PipelinePage/PipelineListsView/AddNewPipeline/FormFields/DescriptionTextArea.tsx`,
|
||||
`components/HeaderRightSection/FeedbackModal.tsx`,
|
||||
`pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelStepDetailsModal.tsx`,
|
||||
`pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelDescriptionModal.tsx`,
|
||||
`pages/TracesExplorer/Filter/SectionContent.tsx`.
|
||||
|
||||
**Destructure `const { TextArea/Search } = Input`:**
|
||||
`Trace/Filters/Panel/PanelBody/CommonCheckBox/index.tsx`,
|
||||
`Trace/Filters/Panel/PanelBody/SearchTraceID/index.tsx`,
|
||||
`Trace/Search/styles.ts`,
|
||||
`ListAlertRules/ListAlert.tsx`,
|
||||
`FormAlertRules/styles.ts`,
|
||||
`FormAlertChannels/Settings/Opsgenie.tsx`,
|
||||
`FormAlertChannels/Settings/Pager.tsx`,
|
||||
`FormAlertChannels/Settings/Slack.tsx`,
|
||||
`NewWidget/RightContainer/SettingSections/GeneralSettingsSection/GeneralSettingsSection.tsx`,
|
||||
`AnomalyAlertEvaluationView/AnomalyAlertEvaluationView.tsx`.
|
||||
|
||||
**`InputNumber` co-used:**
|
||||
`NewWidget/RightContainer/Threshold/Threshold.tsx`,
|
||||
`components/LogsFormatOptionsMenu/LogsFormatOptionsMenu.tsx`.
|
||||
|
||||
**Typed `InputRef`:**
|
||||
`DashboardContainer/DashboardVariablesSelection/TextboxVariableInput.tsx`,
|
||||
`NewWidget/RightContainer/SettingSections/GeneralSettingsSection/GeneralSettingsSection.tsx`,
|
||||
`PipelinePage/components/TagInput.tsx`,
|
||||
`components/OverflowInputToolTip/OverflowInputToolTip.tsx`,
|
||||
`components/CustomTimePicker/CustomTimePicker.tsx`,
|
||||
`components/Input/index.tsx`,
|
||||
`pages/AlertDetails/AlertHeader/ActionButtons/RenameModal.tsx`,
|
||||
`LogsSearchFilter/index.tsx`.
|
||||
|
||||
**Use `addonBefore` / `addonAfter`:**
|
||||
`QueryBuilder/components/Query/Query.tsx`,
|
||||
`QueryBuilder/components/Formula/Formula.tsx`,
|
||||
`GridCardLayout/GridCard/FullView/index.tsx`,
|
||||
`GridCardLayout/WidgetHeader/index.tsx`,
|
||||
`OnboardingV2Container/InviteTeamMembers/InviteTeamMembers.tsx`,
|
||||
`NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse/query.tsx`,
|
||||
`NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query.tsx`,
|
||||
`components/Input/index.tsx`,
|
||||
`pages/TracesExplorer/Filter/DurationSection.tsx`.
|
||||
|
||||
**Use `allowClear`:**
|
||||
`Trace/Search/AllTags/Tag/TagKey.tsx` _(migrated — `allowClear` was only used elsewhere)_,
|
||||
`ServiceTable/Filter/FilterDropdown.tsx`,
|
||||
`ServiceApplication/Filter/FilterDropdown.tsx`,
|
||||
`LogsSearchFilter/index.tsx`,
|
||||
`MetricsExplorer/MetricDetails/AllAttributesValue.tsx`,
|
||||
`GridCardLayout/GridCard/FullView/index.tsx`,
|
||||
`NewWidget/RightContainer/SettingSections/GeneralSettingsSection/GeneralSettingsSection.tsx`,
|
||||
`AnomalyAlertEvaluationView/AnomalyAlertEvaluationView.tsx`,
|
||||
`AllError/index.tsx`,
|
||||
`PipelinePage/Layouts/Pipeline/PipelinesSearchSection.tsx`,
|
||||
`lib/uPlotV2/components/Legend/Legend.tsx`.
|
||||
|
||||
**Use `bordered`, `status`, or `size="small|middle|large"`:**
|
||||
`OrganizationSettings/DisplayName/index.tsx` (size, status),
|
||||
`FormAlertRules/labels/index.tsx` (bordered),
|
||||
`DashboardContainer/DashboardVariablesSelection/TextboxVariableInput.tsx` (bordered),
|
||||
`MetricsExplorer/MetricDetails/AllAttributes.tsx` (size),
|
||||
`MetricsExplorer/MetricDetails/AllAttributesValue.tsx` (size),
|
||||
`AllError/index.tsx` (size),
|
||||
`components/CustomTimePicker/CustomTimePicker.tsx` (status),
|
||||
`QueryBuilder/components/Query/Query.tsx` (size),
|
||||
`QueryBuilder/components/Formula/Formula.tsx` (size),
|
||||
`NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse/query.tsx` (size),
|
||||
`NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query.tsx` (size).
|
||||
|
||||
**`styled(Input)` in `.ts` style files (DOM structure differs):**
|
||||
`Trace/Filters/Panel/PanelBody/Duration/styles.ts`,
|
||||
`Version/styles.ts`.
|
||||
|
||||
**Aliased-only `Input as X` (no plain `<Input>`):**
|
||||
`ResetPassword/index.tsx`.
|
||||
|
||||
**Already on `@signozhq/ui/input`** (no diff this round): Retention, AuthnOIDC/SAML/Google + their helper sections (ClaimMapping, RoleMapping, AttributeMapping, DomainMapping), ForgotPassword, IntegrationsHeader, OnboardingQuestionaire (Invite + AboutSigNoz), AIAssistant/ChatInput, ServiceAccountsSettings, MembersSettings, ServiceAccountDrawer (EditKey + AddKey), InviteMembersModal, EditMemberDrawer, RolesSettings + CreateRoleModal, SignUp, plus the rest of the `@signozhq/ui/input` adopters listed in `git grep "@signozhq/ui/input"`.
|
||||
@@ -166,6 +166,7 @@ function createMockAppContext(
|
||||
userPreferences: [],
|
||||
hostsData: null,
|
||||
isLoggedIn: true,
|
||||
isPreflightLoading: false,
|
||||
org: [{ createdAt: 0, id: 'org-id', displayName: 'Test Org' }],
|
||||
isFetchingUser: false,
|
||||
isFetchingActiveLicense: false,
|
||||
|
||||
@@ -59,6 +59,7 @@ function App(): JSX.Element {
|
||||
isLoggedIn: isLoggedInState,
|
||||
featureFlags,
|
||||
org,
|
||||
isPreflightLoading,
|
||||
} = useAppContext();
|
||||
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
|
||||
const isAIAssistantEnabled = useIsAIAssistantEnabled();
|
||||
@@ -386,6 +387,10 @@ function App(): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isCloudUser, isEnterpriseSelfHostedUser]);
|
||||
|
||||
if (isPreflightLoading) {
|
||||
return <Spinner tip="Loading..." />;
|
||||
}
|
||||
|
||||
// if the user is in logged in state
|
||||
if (isLoggedInState) {
|
||||
// if the setup calls are loading then return a spinner
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import axios from 'axios';
|
||||
import { getIsNoAuthMode } from 'utils/noAuthMode';
|
||||
|
||||
import { interceptorRejected } from '../index';
|
||||
|
||||
jest.mock('utils/noAuthMode', () => ({
|
||||
getIsNoAuthMode: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/v2/sessions/rotate/post', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('AppRoutes/utils', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../utils', () => ({
|
||||
Logout: jest.fn(),
|
||||
}));
|
||||
|
||||
// oxlint-disable-next-line typescript/no-require-imports typescript/no-var-requires
|
||||
const post = require('api/v2/sessions/rotate/post').default;
|
||||
// oxlint-disable-next-line typescript/no-require-imports typescript/no-var-requires
|
||||
const { Logout } = require('../utils');
|
||||
|
||||
describe('interceptorRejected — no-auth mode', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(axios, 'isAxiosError').mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('does NOT call rotate or Logout when no-auth mode is enabled on 401', async () => {
|
||||
(getIsNoAuthMode as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const error = {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
status: 401,
|
||||
config: { url: '/dashboards', method: 'get' },
|
||||
},
|
||||
config: { url: '/dashboards', headers: {} },
|
||||
};
|
||||
|
||||
await interceptorRejected(error as any).catch(() => {});
|
||||
|
||||
expect(post).not.toHaveBeenCalled();
|
||||
expect(Logout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('DOES attempt rotate when no-auth mode is disabled on 401', async () => {
|
||||
(getIsNoAuthMode as jest.Mock).mockReturnValue(false);
|
||||
(post as jest.Mock).mockResolvedValue({
|
||||
data: { accessToken: 'a', refreshToken: 'b' },
|
||||
});
|
||||
|
||||
const error = {
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
status: 401,
|
||||
config: { url: '/dashboards', method: 'get' },
|
||||
},
|
||||
config: { url: '/dashboards', headers: {} },
|
||||
};
|
||||
|
||||
await interceptorRejected(error as any).catch(() => {});
|
||||
|
||||
expect(post).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import { Events } from 'constants/events';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { getBasePath } from 'utils/basePath';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import { getIsNoAuthMode } from 'utils/noAuthMode';
|
||||
|
||||
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
|
||||
import { Logout } from './utils';
|
||||
@@ -108,7 +109,10 @@ export const interceptorRejected = async (
|
||||
if (axios.isAxiosError(value) && value.response) {
|
||||
const { response } = value;
|
||||
|
||||
const isNoAuthMode = getIsNoAuthMode();
|
||||
|
||||
if (
|
||||
!isNoAuthMode &&
|
||||
response.status === 401 &&
|
||||
// if the session rotate call or the create session errors out with 401 or the delete sessions call returns 401 then we do not retry!
|
||||
response.config.url !== '/sessions/rotate' &&
|
||||
@@ -140,16 +144,20 @@ export const interceptorRejected = async (
|
||||
return await Promise.resolve(reResponse);
|
||||
} catch (error) {
|
||||
if ((error as AxiosError)?.response?.status === 401) {
|
||||
Logout();
|
||||
void Logout();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logout();
|
||||
void Logout();
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status === 401 && response.config.url === '/sessions/rotate') {
|
||||
Logout();
|
||||
if (
|
||||
!isNoAuthMode &&
|
||||
response.status === 401 &&
|
||||
response.config.url === '/sessions/rotate'
|
||||
) {
|
||||
void Logout();
|
||||
}
|
||||
}
|
||||
return await Promise.reject(value);
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, Form, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Card, Form } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { X } from '@signozhq/icons';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Input, InputNumber, Popover, Tooltip } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, InputNumber, Popover, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { DefaultOptionType } from 'antd/es/select';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
.banner {
|
||||
height: var(--spacing-20);
|
||||
|
||||
a {
|
||||
color: var(--callout-warning-title);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: var(--callout-warning-title);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
frontend/src/components/NoAuthBanner/NoAuthBanner.tsx
Normal file
26
frontend/src/components/NoAuthBanner/NoAuthBanner.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { PersistedAnnouncementBanner } from '@signozhq/ui/announcement-banner';
|
||||
|
||||
import styles from './NoAuthBanner.module.scss';
|
||||
|
||||
export function NoAuthBanner(): JSX.Element {
|
||||
return (
|
||||
<PersistedAnnouncementBanner
|
||||
type="warning"
|
||||
storageKey="no-auth-banner-v1"
|
||||
testId="no-auth-banner"
|
||||
className={styles.banner}
|
||||
>
|
||||
Impersonation mode: authentication is disabled. Anyone with access to this
|
||||
instance has admin privileges.{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/manage/administrator-guide/configuration/no-auth-mode/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</PersistedAnnouncementBanner>
|
||||
);
|
||||
}
|
||||
|
||||
export default NoAuthBanner;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
import { NoAuthBanner } from '../NoAuthBanner';
|
||||
|
||||
describe('NoAuthBanner', () => {
|
||||
it('renders the no-auth message', () => {
|
||||
render(<NoAuthBanner />);
|
||||
expect(
|
||||
screen.getByText(/Impersonation mode: authentication is disabled/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with the warning test id', () => {
|
||||
render(<NoAuthBanner />);
|
||||
expect(screen.getByTestId('no-auth-banner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a docs link that opens in a new tab', () => {
|
||||
render(<NoAuthBanner />);
|
||||
const link = screen.getByRole('link', { name: /learn more/i });
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link).toHaveAttribute('rel', 'noreferrer');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { Button, Checkbox, Input, Skeleton } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Checkbox, Skeleton } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { KeyRound, X } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Skeleton, Table } from 'antd';
|
||||
import { Skeleton, Table, Tooltip } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table/interface';
|
||||
import type { ServiceaccounttypesGettableFactorAPIKeyDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
|
||||
@@ -110,28 +110,34 @@ function buildColumns({
|
||||
onClick: (e): void => e.stopPropagation(),
|
||||
style: { cursor: 'default' },
|
||||
}),
|
||||
render: (_, record): JSX.Element => (
|
||||
<AuthZTooltip
|
||||
checks={[
|
||||
buildAPIKeyDeletePermission(record.id),
|
||||
buildSADetachPermission(accountId),
|
||||
]}
|
||||
enabled={!isDisabled && !!accountId}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="destructive"
|
||||
disabled={isDisabled}
|
||||
onClick={(): void => {
|
||||
onRevokeClick(record.id);
|
||||
}}
|
||||
className="keys-tab__revoke-btn"
|
||||
render: (_, record): JSX.Element => {
|
||||
const tooltipTitle = isDisabled ? 'Service account disabled' : 'Revoke Key';
|
||||
return (
|
||||
<AuthZTooltip
|
||||
checks={[
|
||||
buildAPIKeyDeletePermission(record.id),
|
||||
buildSADetachPermission(accountId),
|
||||
]}
|
||||
enabled={!isDisabled && !!accountId}
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
</AuthZTooltip>
|
||||
),
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="destructive"
|
||||
disabled={isDisabled}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
onRevokeClick(record.id);
|
||||
}}
|
||||
className="keys-tab__revoke-btn"
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</AuthZTooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Button, Input, Select, Tooltip } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Select, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { CircleX, Trash } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Collapse, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Collapse } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Input, Select } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import { ADVANCED_OPTIONS_TIME_UNIT_OPTIONS } from '../../context/constants';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Input } from 'antd';
|
||||
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import './TimeInput.scss';
|
||||
|
||||
export interface TimeInputProps {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Input, Select } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
Plus,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import { Button, Card, Input, Modal, Popover, Tag, Tooltip } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Card, Modal, Popover, Tag, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { usePanelContextMenu } from '../usePanelContextMenu';
|
||||
|
||||
// The hook composes `useCoordinates` (popover state) and `useGraphContextMenu`
|
||||
// (menu items). We mock both so the test focuses on the `enableDrillDown` gate
|
||||
// rather than the implementation of the menu wiring itself.
|
||||
const onClickMock = jest.fn();
|
||||
jest.mock('periscope/components/ContextMenu', () => ({
|
||||
useCoordinates: (): unknown => ({
|
||||
coordinates: null,
|
||||
popoverPosition: null,
|
||||
clickedData: null,
|
||||
onClose: jest.fn(),
|
||||
subMenu: null,
|
||||
onClick: onClickMock,
|
||||
setSubMenu: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/QueryTable/Drilldown/useGraphContextMenu', () => ({
|
||||
__esModule: true,
|
||||
default: (): { menuItemsConfig: { header: string; items: string } } => ({
|
||||
menuItemsConfig: { header: 'menu-header', items: 'menu-items' },
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/QueryTable/Drilldown/drilldownUtils', () => ({
|
||||
getUplotClickData: jest.fn(() => ({
|
||||
coord: { x: 1, y: 2 },
|
||||
record: { queryName: 'A', filters: [] },
|
||||
label: 'lbl',
|
||||
seriesColor: '#abc',
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('container/PanelWrapper/utils', () => ({
|
||||
isApmMetric: jest.fn(() => false),
|
||||
getTimeRangeFromStepInterval: jest.fn(() => ({ start: 0, end: 0 })),
|
||||
}));
|
||||
|
||||
const mockWidget = { id: 'w-1', query: {} } as unknown as Widgets;
|
||||
const mockQueryResponse = {
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
} as unknown as UseQueryResult<
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
|
||||
describe('usePanelContextMenu', () => {
|
||||
beforeEach(() => {
|
||||
onClickMock.mockClear();
|
||||
});
|
||||
|
||||
it('returns empty menuItemsConfig when enableDrillDown is false', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelContextMenu({
|
||||
widget: mockWidget,
|
||||
queryResponse: mockQueryResponse,
|
||||
enableDrillDown: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.menuItemsConfig).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('returns wired menuItemsConfig when enableDrillDown is true', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelContextMenu({
|
||||
widget: mockWidget,
|
||||
queryResponse: mockQueryResponse,
|
||||
enableDrillDown: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.menuItemsConfig).toStrictEqual({
|
||||
header: 'menu-header',
|
||||
items: 'menu-items',
|
||||
});
|
||||
});
|
||||
|
||||
it('clickHandlerWithContextMenu is a no-op when enableDrillDown is false', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelContextMenu({
|
||||
widget: mockWidget,
|
||||
queryResponse: mockQueryResponse,
|
||||
enableDrillDown: false,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.clickHandlerWithContextMenu(
|
||||
100, // xValue
|
||||
200, // yValue
|
||||
0, // mouseX
|
||||
0, // mouseY
|
||||
{ serviceName: 'svc' }, // metric
|
||||
{ queryName: 'A', inFocusOrNot: true }, // queryData
|
||||
10, // absoluteMouseX
|
||||
20, // absoluteMouseY
|
||||
{}, // axesData
|
||||
{ seriesIndex: 0, seriesName: 'A', value: 1, color: '#abc' }, // focusedSeries
|
||||
);
|
||||
|
||||
expect(onClickMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clickHandlerWithContextMenu opens popover when enableDrillDown is true', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelContextMenu({
|
||||
widget: mockWidget,
|
||||
queryResponse: mockQueryResponse,
|
||||
enableDrillDown: true,
|
||||
}),
|
||||
);
|
||||
|
||||
result.current.clickHandlerWithContextMenu(
|
||||
100,
|
||||
200,
|
||||
0,
|
||||
0,
|
||||
{ serviceName: 'svc' },
|
||||
{ queryName: 'A', inFocusOrNot: true },
|
||||
10,
|
||||
20,
|
||||
{},
|
||||
{ seriesIndex: 0, seriesName: 'A', value: 1, color: '#abc' },
|
||||
);
|
||||
|
||||
expect(onClickMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('defaults to disabled when enableDrillDown is not provided', () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePanelContextMenu({
|
||||
widget: mockWidget,
|
||||
queryResponse: mockQueryResponse,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.menuItemsConfig).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
@@ -21,11 +21,13 @@ interface UseTimeSeriesContextMenuParams {
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
enableDrillDown?: boolean;
|
||||
}
|
||||
|
||||
export const usePanelContextMenu = ({
|
||||
widget,
|
||||
queryResponse,
|
||||
enableDrillDown = false,
|
||||
}: UseTimeSeriesContextMenuParams): {
|
||||
coordinates: { x: number; y: number } | null;
|
||||
popoverPosition: PopoverPosition | null;
|
||||
@@ -61,6 +63,9 @@ export const usePanelContextMenu = ({
|
||||
|
||||
const clickHandlerWithContextMenu = useCallback(
|
||||
(...args: any[]) => {
|
||||
if (!enableDrillDown) {
|
||||
return;
|
||||
}
|
||||
const [
|
||||
xValue,
|
||||
_yvalue,
|
||||
@@ -112,14 +117,14 @@ export const usePanelContextMenu = ({
|
||||
});
|
||||
}
|
||||
},
|
||||
[onClick, queryResponse],
|
||||
[enableDrillDown, onClick, queryResponse],
|
||||
);
|
||||
|
||||
return {
|
||||
coordinates,
|
||||
popoverPosition,
|
||||
onClose,
|
||||
menuItemsConfig,
|
||||
menuItemsConfig: enableDrillDown ? menuItemsConfig : {},
|
||||
clickHandlerWithContextMenu,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
isFullViewMode,
|
||||
onToggleModelHandler,
|
||||
groupByPerQuery,
|
||||
enableDrillDown = false,
|
||||
} = props;
|
||||
const uPlotRef = useRef<uPlot | null>(null);
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
@@ -61,6 +62,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
} = usePanelContextMenu({
|
||||
widget,
|
||||
queryResponse,
|
||||
enableDrillDown,
|
||||
});
|
||||
|
||||
const config = useMemo(() => {
|
||||
|
||||
@@ -31,6 +31,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
isFullViewMode,
|
||||
onToggleModelHandler,
|
||||
groupByPerQuery,
|
||||
enableDrillDown = false,
|
||||
} = props;
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
@@ -60,6 +61,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
} = usePanelContextMenu({
|
||||
widget,
|
||||
queryResponse,
|
||||
enableDrillDown,
|
||||
});
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
|
||||
@@ -19,11 +19,11 @@ import {
|
||||
Info,
|
||||
} from '@signozhq/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import {
|
||||
Button,
|
||||
ColorPicker,
|
||||
Divider,
|
||||
Input,
|
||||
Modal,
|
||||
RefSelectProps,
|
||||
Select,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input } from 'antd';
|
||||
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Form } from 'antd';
|
||||
import { EmailChannel } from '../../CreateAlertChannels/config';
|
||||
|
||||
function EmailForm({ setSelectedConfig }: EmailFormProps): JSX.Element {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Form } from 'antd';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
|
||||
import { WebhookChannel } from '../../CreateAlertChannels/config';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Dispatch, ReactElement, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, FormInstance, Input, Select, Switch } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Form, FormInstance, Select, Switch } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { Store } from 'antd/lib/form/interface';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
@@ -292,6 +292,8 @@ function FullView({
|
||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||
}
|
||||
|
||||
const showEditBtn = editWidget && dashboardEditView;
|
||||
|
||||
return (
|
||||
<div className="full-view-container">
|
||||
<OverlayScrollbar>
|
||||
@@ -306,7 +308,7 @@ function FullView({
|
||||
Reset Query
|
||||
</Button>
|
||||
)}
|
||||
{editWidget && (
|
||||
{showEditBtn && (
|
||||
<Button
|
||||
className="switch-edit-btn"
|
||||
disabled={response.isFetching || response.isLoading}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { useIsFetching } from 'react-query';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Form, Input, Modal } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Form, Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -18,6 +18,8 @@ import listUserPreferences from 'api/v1/user/preferences/list';
|
||||
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
|
||||
import Header from 'components/Header/Header';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import NoAuthBanner from 'components/NoAuthBanner/NoAuthBanner';
|
||||
import { getIsNoAuthMode } from 'utils/noAuthMode';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { ORG_PREFERENCES } from 'constants/orgPreferences';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -196,7 +198,7 @@ export default function Home(): JSX.Element {
|
||||
const { mutate: updateUserPreference } = useMutation(updateUserPreferenceAPI, {
|
||||
onSuccess: () => {
|
||||
setUpdatingUserPreferences(false);
|
||||
refetchUserPreferences();
|
||||
void refetchUserPreferences();
|
||||
},
|
||||
onError: () => {
|
||||
setUpdatingUserPreferences(false);
|
||||
@@ -204,7 +206,7 @@ export default function Home(): JSX.Element {
|
||||
});
|
||||
|
||||
const handleWillDoThisLater = (): void => {
|
||||
logEvent('Welcome Checklist: Will do this later clicked', {});
|
||||
void logEvent('Welcome Checklist: Will do this later clicked', {});
|
||||
setUpdatingUserPreferences(true);
|
||||
|
||||
updateUserPreference({
|
||||
@@ -271,11 +273,12 @@ export default function Home(): JSX.Element {
|
||||
}, [metricsOnboardingData, handleUpdateChecklistDoneItem]);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent('Homepage: Visited', {});
|
||||
void logEvent('Homepage: Visited', {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="home-container">
|
||||
{getIsNoAuthMode() && <NoAuthBanner />}
|
||||
<div className="sticky-header">
|
||||
<Header
|
||||
leftComponent={
|
||||
@@ -298,9 +301,9 @@ export default function Home(): JSX.Element {
|
||||
autoAdjustOverflow
|
||||
onOpenChange={(visible): void => {
|
||||
if (visible) {
|
||||
logEvent('Welcome Checklist: Expanded', {});
|
||||
void logEvent('Welcome Checklist: Expanded', {});
|
||||
} else {
|
||||
logEvent('Welcome Checklist: Minimized', {});
|
||||
void logEvent('Welcome Checklist: Minimized', {});
|
||||
}
|
||||
}}
|
||||
content={renderWelcomeChecklistModal()}
|
||||
@@ -353,7 +356,7 @@ export default function Home(): JSX.Element {
|
||||
className="active-ingestion-card-actions"
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
void logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
safeNavigate(ROUTES.LOGS_EXPLORER, {
|
||||
@@ -362,7 +365,7 @@ export default function Home(): JSX.Element {
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
void logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
@@ -396,7 +399,7 @@ export default function Home(): JSX.Element {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
void logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
safeNavigate(ROUTES.TRACES_EXPLORER, {
|
||||
@@ -405,7 +408,7 @@ export default function Home(): JSX.Element {
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
void logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
@@ -439,7 +442,7 @@ export default function Home(): JSX.Element {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
void logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
safeNavigate(ROUTES.METRICS_EXPLORER, {
|
||||
@@ -448,7 +451,7 @@ export default function Home(): JSX.Element {
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
void logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
history.push(ROUTES.METRICS_EXPLORER);
|
||||
@@ -496,7 +499,7 @@ export default function Home(): JSX.Element {
|
||||
className="periscope-btn secondary"
|
||||
prefix={<Wrench size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
void logEvent('Homepage: Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
safeNavigate(ROUTES.LOGS_EXPLORER, {
|
||||
@@ -513,7 +516,7 @@ export default function Home(): JSX.Element {
|
||||
className="periscope-btn secondary"
|
||||
prefix={<Wrench size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
void logEvent('Homepage: Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
safeNavigate(ROUTES.TRACES_EXPLORER, {
|
||||
@@ -530,7 +533,7 @@ export default function Home(): JSX.Element {
|
||||
className="periscope-btn secondary"
|
||||
prefix={<Wrench size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
void logEvent('Homepage: Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
safeNavigate(ROUTES.METRICS_EXPLORER_EXPLORER, {
|
||||
@@ -569,7 +572,7 @@ export default function Home(): JSX.Element {
|
||||
className="periscope-btn secondary"
|
||||
prefix={<Plus size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
void logEvent('Homepage: Explore clicked', {
|
||||
source: 'Dashboards',
|
||||
});
|
||||
safeNavigate(ROUTES.ALL_DASHBOARD, {
|
||||
@@ -614,7 +617,7 @@ export default function Home(): JSX.Element {
|
||||
className="periscope-btn secondary"
|
||||
prefix={<Plus size={14} />}
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
void logEvent('Homepage: Explore clicked', {
|
||||
source: 'Alerts',
|
||||
});
|
||||
safeNavigate(ROUTES.ALERTS_NEW, {
|
||||
|
||||
@@ -5,12 +5,12 @@ import { useCopyToClipboard } from 'react-use';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import {
|
||||
Col,
|
||||
Collapse,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Row,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Form, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Form } from 'antd';
|
||||
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
function RenderConnectionFields({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Form } from 'antd';
|
||||
import apply from 'api/v3/licenses/post';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { ChangeEvent, useState } from 'react';
|
||||
import { Button, Input, Modal } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import ApacheIcon from 'assets/CustomIcons/ApacheIcon';
|
||||
import DockerIcon from 'assets/CustomIcons/DockerIcon';
|
||||
|
||||
@@ -12,11 +12,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Flex,
|
||||
Input,
|
||||
MenuProps,
|
||||
Modal,
|
||||
Popover,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoaderCircle, Check } from '@signozhq/icons';
|
||||
import { Button, Input, Space } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
|
||||
@@ -2,7 +2,8 @@ import { ReactNode, useState } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Collapse, Divider, Input, Switch, Tag } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Collapse, Divider, Switch, Tag } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
|
||||
@@ -2,7 +2,8 @@ import { ChangeEvent, useCallback, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CirclePlus, X } from '@signozhq/icons';
|
||||
import { Col, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Col } from 'antd';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { SquareX, X } from '@signozhq/icons';
|
||||
import { Button, Input, Select } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Select } from 'antd';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import {
|
||||
ConditionalOperators,
|
||||
|
||||
@@ -21,7 +21,6 @@ const PAGE_SIZE = 20;
|
||||
function MembersSettings(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const pageParam = parseInt(urlQuery.get('page') ?? '1', 10);
|
||||
const currentPage = Number.isNaN(pageParam) || pageParam < 1 ? 1 : pageParam;
|
||||
|
||||
@@ -146,7 +145,7 @@ function MembersSettings(): JSX.Element {
|
||||
: `Deleted ⎯ ${deletedCount}`;
|
||||
|
||||
const handleInviteComplete = useCallback((): void => {
|
||||
refetchUsers();
|
||||
void refetchUsers();
|
||||
}, [refetchUsers]);
|
||||
|
||||
const handleRowClick = useCallback((member: MemberRow): void => {
|
||||
@@ -158,7 +157,7 @@ function MembersSettings(): JSX.Element {
|
||||
}, []);
|
||||
|
||||
const handleMemberEditComplete = useCallback((): void => {
|
||||
refetchUsers();
|
||||
void refetchUsers();
|
||||
}, [refetchUsers]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -30,7 +30,12 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { GraphTitle, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
|
||||
import {
|
||||
GraphTitle,
|
||||
MENU_ITEMS,
|
||||
SERVICE_CHART_ID,
|
||||
SERVICE_DETAIL_DRILLDOWN_ENABLED,
|
||||
} from '../constant';
|
||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||
import { Card, GraphContainer, Row } from '../styles';
|
||||
import { Button } from './styles';
|
||||
@@ -206,6 +211,7 @@ function DBCall(): JSX.Element {
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
@@ -244,6 +250,7 @@ function DBCall(): JSX.Element {
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
|
||||
@@ -32,7 +32,12 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { useAppContext } from '../../../providers/App/App';
|
||||
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
|
||||
import {
|
||||
GraphTitle,
|
||||
legend,
|
||||
MENU_ITEMS,
|
||||
SERVICE_DETAIL_DRILLDOWN_ENABLED,
|
||||
} from '../constant';
|
||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||
import { Card, GraphContainer, Row } from '../styles';
|
||||
import GraphControlsPanel from './Overview/GraphControlsPanel/GraphControlsPanel';
|
||||
@@ -279,6 +284,7 @@ function External(): JSX.Element {
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
@@ -322,6 +328,7 @@ function External(): JSX.Element {
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
@@ -366,6 +373,7 @@ function External(): JSX.Element {
|
||||
}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
@@ -409,6 +417,7 @@ function External(): JSX.Element {
|
||||
}}
|
||||
onDragSelect={onDragSelect}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
|
||||
@@ -15,6 +15,7 @@ import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThres
|
||||
import {
|
||||
GraphTitle,
|
||||
SERVICE_CHART_ID,
|
||||
SERVICE_DETAIL_DRILLDOWN_ENABLED,
|
||||
} from 'container/MetricsApplication/constant';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
|
||||
@@ -105,6 +106,7 @@ function ApDexMetrics({
|
||||
threshold={threshold}
|
||||
isQueryEnabled={isQueryEnabled}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Graph from 'container/GridCardLayout/GridCard';
|
||||
import {
|
||||
GraphTitle,
|
||||
SERVICE_CHART_ID,
|
||||
SERVICE_DETAIL_DRILLDOWN_ENABLED,
|
||||
} from 'container/MetricsApplication/constant';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
|
||||
@@ -138,6 +139,7 @@ function ServiceOverview({
|
||||
onClickHandler={handleGraphClick('Service')}
|
||||
isQueryEnabled={isQueryEnabled}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
)}
|
||||
</GraphContainer>
|
||||
|
||||
@@ -4,6 +4,7 @@ import axios from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import { SERVICE_DETAIL_DRILLDOWN_ENABLED } from 'container/MetricsApplication/constant';
|
||||
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
@@ -43,6 +44,7 @@ function TopLevelOperation({
|
||||
onDragSelect={onDragSelect}
|
||||
isQueryEnabled={!topLevelOperationsIsLoading}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
)}
|
||||
</GraphContainer>
|
||||
|
||||
@@ -25,6 +25,8 @@ export const OPERATION_LEGENDS = ['Operations'];
|
||||
|
||||
export const MENU_ITEMS = [MenuItemKeys.View, MenuItemKeys.CreateAlerts];
|
||||
|
||||
export const SERVICE_DETAIL_DRILLDOWN_ENABLED = true;
|
||||
|
||||
export enum FORMULA {
|
||||
ERROR_PERCENTAGE = 'A*100/B',
|
||||
DATABASE_CALLS_AVG_DURATION = 'A/B',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Input } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Select } from 'antd';
|
||||
// TODO(@signozhq/ui-input): migrate this <Input> once @signozhq/ui Input
|
||||
// supports the `onWheel` handler (used to blur on scroll for number inputs).
|
||||
import { Input, Select } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { TIME_AGGREGATION_OPTIONS } from './constants';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import type { TableColumnsType as ColumnsType } from 'antd';
|
||||
import { Button, Collapse, Input, Select, Spin } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Collapse, Select, Spin } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
|
||||
@@ -80,10 +80,10 @@ function UserInfo(): JSX.Element {
|
||||
currentPassword === updatePassword;
|
||||
|
||||
const onSaveHandler = async (): Promise<void> => {
|
||||
logEvent('Account Settings: Name Updated', {
|
||||
void logEvent('Account Settings: Name Updated', {
|
||||
name: changedName,
|
||||
});
|
||||
logEvent(
|
||||
void logEvent(
|
||||
'Account Settings: Name Updated',
|
||||
{
|
||||
name: changedName,
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
DropResult,
|
||||
} from 'react-beautiful-dnd';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Dropdown, Input, MenuProps, Tooltip } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Divider, Dropdown, MenuProps, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { FieldDataType } from 'api/v5/v5';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Col, Form, Input as AntInput, Input, Row } from 'antd';
|
||||
// TODO(@signozhq/ui-input): migrate <Input> once @signozhq/ui Input
|
||||
// supports the `spellCheck` prop on the URL input below.
|
||||
import { Button, Col, Form, Input, Input as AntInput, Row } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { CONTEXT_LINK_FIELDS } from 'container/NewWidget/RightContainer/ContextLinks/constants';
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Blocks, Check, LoaderCircle } from '@signozhq/icons';
|
||||
import { Button, Card, Form, Input, Select, Space } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Card, Form, Select, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Check, Server, LoaderCircle } from '@signozhq/icons';
|
||||
import { Button, Card, Form, Input, Space } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Card, Form, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -209,7 +209,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.STARTED}`,
|
||||
{},
|
||||
);
|
||||
@@ -253,7 +253,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
setSelectedFramework(null);
|
||||
setSelectedEnvironment(null);
|
||||
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_SELECTED}`,
|
||||
{
|
||||
dataSource: dataSource.label,
|
||||
@@ -276,7 +276,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
};
|
||||
|
||||
const handleSelectFramework = (option: any): void => {
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.FRAMEWORK_SELECTED}`,
|
||||
{
|
||||
dataSource: selectedDataSource?.label,
|
||||
@@ -309,7 +309,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
selectedEnvironment: any,
|
||||
baseURL?: string,
|
||||
): void => {
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.ENVIRONMENT_SELECTED}`,
|
||||
{
|
||||
dataSource: selectedDataSource?.label,
|
||||
@@ -351,7 +351,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
groupDataSourcesByTags(filteredDataSources as Entity[]),
|
||||
);
|
||||
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_SEARCHED}`,
|
||||
{
|
||||
searchedDataSource: query,
|
||||
@@ -485,7 +485,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
};
|
||||
|
||||
const handleShowInviteTeamMembersModal = (): void => {
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.INVITE_TEAM_MEMBER_BUTTON_CLICKED}`,
|
||||
{
|
||||
dataSource: selectedDataSource?.label,
|
||||
@@ -498,7 +498,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
};
|
||||
|
||||
const handleSubmitDataSourceRequest = (): void => {
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_REQUESTED}`,
|
||||
{
|
||||
requestedDataSource: dataSourceRequest,
|
||||
@@ -513,7 +513,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
};
|
||||
|
||||
const handleRaiseRequest = (): void => {
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_REQUESTED}`,
|
||||
{
|
||||
requestedDataSource: searchQuery,
|
||||
@@ -635,7 +635,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
size={14}
|
||||
className="onboarding-header-container-close-icon"
|
||||
onClick={(e): void => {
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CLOSE_ONBOARDING_CLICKED}`,
|
||||
{
|
||||
currentPage: setupStepItems[currentStep]?.title || '',
|
||||
@@ -970,7 +970,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
disabled={!selectedDataSource}
|
||||
shape="round"
|
||||
onClick={(e): void => {
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONFIGURED_PRODUCT}`,
|
||||
{
|
||||
dataSource: selectedDataSource?.label,
|
||||
@@ -1038,7 +1038,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
type="default"
|
||||
shape="round"
|
||||
onClick={(): void => {
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BACK_BUTTON_CLICKED}`,
|
||||
{
|
||||
dataSource: selectedDataSource?.label,
|
||||
@@ -1057,7 +1057,7 @@ function OnboardingAddDataSource(): JSX.Element {
|
||||
type="primary"
|
||||
shape="round"
|
||||
onClick={(e): void => {
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONTINUE_BUTTON_CLICKED}`,
|
||||
{
|
||||
dataSource: selectedDataSource?.label,
|
||||
|
||||
@@ -75,7 +75,7 @@ function AuthDomain(): JSX.Element {
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success('Domain deleted successfully');
|
||||
refetchAuthDomainListResponse();
|
||||
void refetchAuthDomainListResponse();
|
||||
hideDeleteModal();
|
||||
},
|
||||
onError: (error) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus, Trash2 } from '@signozhq/icons';
|
||||
import { Button, Form, FormInstance, Input, Select, Space } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Form, FormInstance, Select, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input } from 'antd';
|
||||
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Form } from 'antd';
|
||||
import { ProcessorFormField } from '../../AddNewProcessor/config';
|
||||
import { formValidationRules } from '../../config';
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
// TODO(@signozhq/ui-input): migrate to @signozhq/ui Input once the antd
|
||||
// `InputProps` spread (`size`, etc.) is no longer needed on this wrapper.
|
||||
import { Input, InputProps } from 'antd';
|
||||
|
||||
function CSVInput({ value, onChange, ...otherProps }: InputProps): JSX.Element {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Info } from '@signozhq/icons';
|
||||
import { Flex, Form, Input, Space, Switch, Tooltip } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Flex, Form, Space, Switch, Tooltip } from 'antd';
|
||||
import { ProcessorData } from 'types/api/pipeline/def';
|
||||
|
||||
import { PREDEFINED_MAPPING } from '../config';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Select, Space, Switch } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Form, Select, Space, Switch } from 'antd';
|
||||
import { ModalFooterTitle } from 'container/PipelinePage/styles';
|
||||
import { ProcessorData } from 'types/api/pipeline/def';
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import React, { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Plus, Search } from '@signozhq/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Flex, Form, Input, Tooltip } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Flex, Form, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
useDeleteDowntimeScheduleByID,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Input, Skeleton } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Skeleton } from 'antd';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';
|
||||
|
||||
@@ -169,9 +169,10 @@ describe('drilldownUtils', () => {
|
||||
|
||||
// Verify transformations were applied
|
||||
if (filterExpression) {
|
||||
// Rule 2: operation → name
|
||||
expect(filterExpression).toContain(`name = 'GET'`);
|
||||
// `operation` rewrites to `name` via source-side pass, then `name`
|
||||
// is dropped by the logs target-side pass (logs has no span-name).
|
||||
expect(filterExpression).not.toContain(`operation = 'GET'`);
|
||||
expect(filterExpression).not.toContain(`name = 'GET'`);
|
||||
|
||||
// Rule 3: span.kind → kind
|
||||
expect(filterExpression).toContain(`${spanKindKey} = '${spanKindServer}'`);
|
||||
@@ -262,8 +263,9 @@ describe('drilldownUtils', () => {
|
||||
const filterExpression = result?.builder.queryData[0]?.filter?.expression;
|
||||
|
||||
if (filterExpression) {
|
||||
// All transformations should be applied
|
||||
expect(filterExpression).toContain(`name = 'POST'`);
|
||||
// `operation` rewrites to `name` then drops for logs target.
|
||||
expect(filterExpression).not.toContain(`operation = 'POST'`);
|
||||
expect(filterExpression).not.toContain(`name = 'POST'`);
|
||||
expect(filterExpression).toContain(`${spanKindKey} = '${spanKindClient}'`);
|
||||
expect(filterExpression).toContain(`status_code_string = 'Error'`);
|
||||
expect(filterExpression).toContain(`http.status_code = 500`);
|
||||
@@ -410,8 +412,9 @@ describe('drilldownUtils', () => {
|
||||
const filterExpression = result?.builder.queryData[0]?.filter?.expression;
|
||||
|
||||
if (filterExpression) {
|
||||
// Transformed attributes
|
||||
expect(filterExpression).toContain(`name = 'GET'`);
|
||||
// `operation` rewrites to `name` then drops for logs target.
|
||||
expect(filterExpression).not.toContain(`operation = 'GET'`);
|
||||
expect(filterExpression).not.toContain(`name = 'GET'`);
|
||||
expect(filterExpression).toContain(`${spanKindKey} = '${spanKindServer}'`);
|
||||
|
||||
// Preserved non-metric attributes
|
||||
@@ -499,4 +502,189 @@ describe('drilldownUtils', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewQuery target-aware sanitisation (serviceName / name)', () => {
|
||||
const makeQuery = (
|
||||
expression: string,
|
||||
dataSource: 'traces' | 'logs' | 'metrics' = 'traces',
|
||||
): Query => ({
|
||||
id: 'src-query',
|
||||
queryType: 'builder' as any,
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: 'src',
|
||||
dataSource: dataSource as any,
|
||||
aggregations: [{ metricName: 'non_apm_metric' }] as any,
|
||||
groupBy: [],
|
||||
expression: '',
|
||||
disabled: false,
|
||||
functions: [],
|
||||
legend: '',
|
||||
having: [],
|
||||
limit: null,
|
||||
stepInterval: undefined,
|
||||
orderBy: [],
|
||||
filter: { expression },
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
});
|
||||
|
||||
it('rewrites serviceName -> service.name when drilling to logs', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`serviceName = 'svc'`),
|
||||
[],
|
||||
'view_logs',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain(`service.name = 'svc'`);
|
||||
expect(expr).not.toContain('serviceName');
|
||||
});
|
||||
|
||||
it('rewrites serviceName -> service.name when drilling to traces', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`serviceName = 'svc'`),
|
||||
[],
|
||||
'view_traces',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain(`service.name = 'svc'`);
|
||||
expect(expr).not.toContain('serviceName');
|
||||
});
|
||||
|
||||
it('drops `name` clause when drilling to logs', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`name = 'GET /api'`),
|
||||
[],
|
||||
'view_logs',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).not.toContain(`name = 'GET /api'`);
|
||||
});
|
||||
|
||||
it('keeps `name` clause when drilling to traces', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`name = 'GET /api'`),
|
||||
[],
|
||||
'view_traces',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain(`name = 'GET /api'`);
|
||||
});
|
||||
|
||||
it('combined: drilling to logs rewrites serviceName and drops name', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`serviceName = 'svc' AND name = 'GET /api'`),
|
||||
[],
|
||||
'view_logs',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain(`service.name = 'svc'`);
|
||||
expect(expr).not.toContain('serviceName');
|
||||
expect(expr).not.toContain(`name = 'GET /api'`);
|
||||
});
|
||||
|
||||
it('combined: drilling to traces rewrites serviceName and keeps name', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`serviceName = 'svc' AND name = 'GET /api'`),
|
||||
[],
|
||||
'view_traces',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain(`service.name = 'svc'`);
|
||||
expect(expr).toContain(`name = 'GET /api'`);
|
||||
expect(expr).not.toContain('serviceName');
|
||||
});
|
||||
|
||||
it('metric-APM source -> traces target preserves existing operation -> name rewrite', () => {
|
||||
const metricsQuery: Query = {
|
||||
id: 'apm-metrics',
|
||||
queryType: 'builder' as any,
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: 'm',
|
||||
dataSource: 'metrics' as any,
|
||||
aggregations: [{ metricName: 'signoz_calls_total' }] as any,
|
||||
groupBy: [],
|
||||
expression: '',
|
||||
disabled: false,
|
||||
functions: [],
|
||||
legend: '',
|
||||
having: [],
|
||||
limit: null,
|
||||
stepInterval: undefined,
|
||||
orderBy: [],
|
||||
filter: { expression: `operation = 'GET'` },
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
};
|
||||
const result = getViewQuery(metricsQuery, [], 'view_traces', 'm');
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain(`name = 'GET'`);
|
||||
expect(expr).not.toContain(`operation = 'GET'`);
|
||||
});
|
||||
|
||||
it('metric-APM source -> logs target: operation rewrites to name, then dropped', () => {
|
||||
const metricsQuery: Query = {
|
||||
id: 'apm-metrics',
|
||||
queryType: 'builder' as any,
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: 'm',
|
||||
dataSource: 'metrics' as any,
|
||||
aggregations: [{ metricName: 'signoz_calls_total' }] as any,
|
||||
groupBy: [],
|
||||
expression: '',
|
||||
disabled: false,
|
||||
functions: [],
|
||||
legend: '',
|
||||
having: [],
|
||||
limit: null,
|
||||
stepInterval: undefined,
|
||||
orderBy: [],
|
||||
filter: { expression: `operation = 'GET'` },
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
};
|
||||
const result = getViewQuery(metricsQuery, [], 'view_logs', 'm');
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).not.toContain(`operation = 'GET'`);
|
||||
expect(expr).not.toContain(`name = 'GET'`);
|
||||
});
|
||||
|
||||
it('drilling to metrics does not apply target-side sanitisation', () => {
|
||||
const result = getViewQuery(
|
||||
makeQuery(`serviceName = 'svc' AND name = 'GET /api'`),
|
||||
[],
|
||||
'view_metrics',
|
||||
'src',
|
||||
);
|
||||
const expr = result?.builder.queryData[0]?.filter?.expression || '';
|
||||
expect(expr).toContain('serviceName');
|
||||
expect(expr).toContain(`name = 'GET /api'`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
import ROUTES from 'constants/routes';
|
||||
import { isApmMetric } from 'container/PanelWrapper/utils';
|
||||
import {
|
||||
applyMappingsToExpression,
|
||||
DRILLDOWN_TO_LOGS_MAPPINGS,
|
||||
DRILLDOWN_TO_TRACES_MAPPINGS,
|
||||
METRIC_TO_LOGS_TRACES_MAPPINGS,
|
||||
replaceKeysAndValuesInExpression,
|
||||
} from 'container/QueryTable/Drilldown/metricsCorrelationUtils';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import {
|
||||
@@ -347,27 +349,41 @@ export const getViewQuery = (
|
||||
newQuery.builder.queryData[0].filter = newFilterExpression;
|
||||
|
||||
try {
|
||||
// ===========================================
|
||||
// TEMP LOGIC - TO BE REMOVED LATER
|
||||
// ===========================================
|
||||
// Apply metric-to-logs/traces transformations
|
||||
// Drill-down filter sanitisation. Two stages:
|
||||
// 1. Source-side: rewrite metric-APM-specific keys (operation, span.kind,
|
||||
// status.code) so they map onto trace/log columns.
|
||||
// 2. Target-side: normalise legacy keys to OTel-canonical (`serviceName`
|
||||
// -> `service.name`) and drop keys with no equivalent in the target
|
||||
// datasource (e.g. `name` for logs).
|
||||
let expression = newFilterExpression?.expression || '';
|
||||
|
||||
const specificQuery = getQueryData(query, queryName);
|
||||
const isMetricQuery = specificQuery?.dataSource === 'metrics';
|
||||
const metricName = (specificQuery?.aggregations?.[0] as MetricAggregation)
|
||||
?.metricName;
|
||||
|
||||
if (isMetricQuery && isApmMetric(metricName || '')) {
|
||||
const transformedExpression = replaceKeysAndValuesInExpression(
|
||||
newFilterExpression?.expression || '',
|
||||
expression = applyMappingsToExpression(
|
||||
expression,
|
||||
METRIC_TO_LOGS_TRACES_MAPPINGS,
|
||||
);
|
||||
newQuery.builder.queryData[0].filter = {
|
||||
expression: transformedExpression || '',
|
||||
};
|
||||
}
|
||||
// ===========================================
|
||||
|
||||
if (key === 'view_logs') {
|
||||
expression = applyMappingsToExpression(
|
||||
expression,
|
||||
DRILLDOWN_TO_LOGS_MAPPINGS,
|
||||
);
|
||||
} else if (key === 'view_traces') {
|
||||
expression = applyMappingsToExpression(
|
||||
expression,
|
||||
DRILLDOWN_TO_TRACES_MAPPINGS,
|
||||
);
|
||||
}
|
||||
|
||||
newQuery.builder.queryData[0].filter = { expression };
|
||||
} catch (error) {
|
||||
console.error('Error transforming metrics to logs/traces:', error);
|
||||
console.error('Error sanitising drilldown filter expression:', error);
|
||||
}
|
||||
|
||||
return newQuery;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
formatValueForExpression,
|
||||
removeKeysFromExpression,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { IQueryPair } from 'types/antlrQueryTypes';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
@@ -8,7 +11,7 @@ import { isFunctionOperator, isNonValueOperator } from 'utils/tokenUtils';
|
||||
|
||||
type KeyValueMapping = {
|
||||
attribute: string;
|
||||
newAttribute: string;
|
||||
newAttribute: string | null;
|
||||
valueMappings: Record<string, string>;
|
||||
};
|
||||
|
||||
@@ -40,8 +43,33 @@ export const METRIC_TO_LOGS_TRACES_MAPPINGS: KeyValueMapping[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const DRILLDOWN_TO_LOGS_MAPPINGS: KeyValueMapping[] = [
|
||||
{
|
||||
attribute: 'serviceName',
|
||||
newAttribute: 'service.name',
|
||||
valueMappings: {},
|
||||
},
|
||||
{
|
||||
attribute: 'name',
|
||||
newAttribute: null,
|
||||
valueMappings: {},
|
||||
},
|
||||
];
|
||||
|
||||
export const DRILLDOWN_TO_TRACES_MAPPINGS: KeyValueMapping[] = [
|
||||
{
|
||||
attribute: 'serviceName',
|
||||
newAttribute: 'service.name',
|
||||
valueMappings: {},
|
||||
},
|
||||
];
|
||||
|
||||
// Logic for rewriting key/values in an expression using provided mappings.
|
||||
function modifyKeyVal(pair: IQueryPair, mapping: KeyValueMapping): string {
|
||||
// Callers must pre-filter mappings to ensure newAttribute is non-null.
|
||||
function modifyKeyVal(
|
||||
pair: IQueryPair,
|
||||
mapping: KeyValueMapping & { newAttribute: string },
|
||||
): string {
|
||||
const newKey = mapping.newAttribute;
|
||||
const op = pair.operator;
|
||||
|
||||
@@ -107,8 +135,18 @@ export function replaceKeysAndValuesInExpression(
|
||||
return expression;
|
||||
}
|
||||
|
||||
const attributeToMapping = new Map<string, KeyValueMapping>(
|
||||
mappingList.map((m) => [m.attribute.trim().toLowerCase(), m]),
|
||||
// Only rewrite mappings (newAttribute non-null) are processed here.
|
||||
// Drops are handled separately by applyMappingsToExpression via removeKeysFromExpression.
|
||||
const attributeToMapping = new Map<
|
||||
string,
|
||||
KeyValueMapping & { newAttribute: string }
|
||||
>(
|
||||
mappingList
|
||||
.filter(
|
||||
(m): m is KeyValueMapping & { newAttribute: string } =>
|
||||
m.newAttribute !== null,
|
||||
)
|
||||
.map((m) => [m.attribute.trim().toLowerCase(), m]),
|
||||
);
|
||||
|
||||
const pairs: IQueryPair[] = extractQueryPairs(expression);
|
||||
@@ -179,3 +217,26 @@ export function replaceKeysAndValuesInExpression(
|
||||
|
||||
return resultParts.join('');
|
||||
}
|
||||
|
||||
// Apply a list of mappings to a filter expression. Rewrites are applied first
|
||||
// (newAttribute is a string), then drops (newAttribute is null) via the
|
||||
// ANTLR-parser-based removeKeysFromExpression which handles AND/OR/NOT/paren
|
||||
// elision correctly.
|
||||
export function applyMappingsToExpression(
|
||||
expression: string,
|
||||
mappings: KeyValueMapping[],
|
||||
): string {
|
||||
if (!expression || !mappings || mappings.length === 0) {
|
||||
return expression;
|
||||
}
|
||||
|
||||
const dropKeys = mappings
|
||||
.filter((m) => m.newAttribute === null)
|
||||
.map((m) => m.attribute);
|
||||
|
||||
let result = replaceKeysAndValuesInExpression(expression, mappings);
|
||||
if (dropKeys.length > 0) {
|
||||
result = removeKeysFromExpression(result, dropKeys);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ChangeEvent, useMemo } from 'react';
|
||||
import { Plus, Search } from '@signozhq/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Flex, Input, Tooltip } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Flex, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
@@ -101,6 +101,7 @@ export function getAppContextMockState(
|
||||
userPreferences: null,
|
||||
hostsData: null,
|
||||
isLoggedIn: false,
|
||||
isPreflightLoading: false,
|
||||
org: null,
|
||||
isFetchingUser: false,
|
||||
isFetchingActiveLicense: false,
|
||||
|
||||
@@ -1120,6 +1120,7 @@
|
||||
|
||||
.user-settings-dropdown-logout-section {
|
||||
color: var(--danger-background);
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,7 +408,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
);
|
||||
|
||||
const handleReorderShortcutNavItems = (): void => {
|
||||
logEvent('Sidebar V2: Save shortcuts clicked', {
|
||||
void logEvent('Sidebar V2: Save shortcuts clicked', {
|
||||
shortcuts: tempPinnedMenuItems.map((item) => item.key),
|
||||
});
|
||||
setPinnedMenuItems(tempPinnedMenuItems);
|
||||
@@ -436,7 +436,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
|
||||
|
||||
const onClickGetStarted = (event: MouseEvent): void => {
|
||||
logEvent('Sidebar: Menu clicked', {
|
||||
void logEvent('Sidebar: Menu clicked', {
|
||||
menuRoute: '/get-started',
|
||||
menuLabel: 'Get Started',
|
||||
});
|
||||
@@ -651,7 +651,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
} else if (item) {
|
||||
onClickHandler(item?.key as string, event);
|
||||
}
|
||||
logEvent('Sidebar V2: Menu clicked', {
|
||||
void logEvent('Sidebar V2: Menu clicked', {
|
||||
menuRoute: item?.key,
|
||||
menuLabel: item?.label,
|
||||
});
|
||||
@@ -794,7 +794,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
onTogglePin={
|
||||
allowPin
|
||||
? (item): void => {
|
||||
logEvent(
|
||||
void logEvent(
|
||||
`Sidebar V2: Menu item ${item.isPinned ? 'unpinned' : 'pinned'}`,
|
||||
{
|
||||
menuRoute: item.key,
|
||||
@@ -841,7 +841,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;
|
||||
|
||||
if (item && !('type' in item)) {
|
||||
logEvent('Help Popover: Item clicked', {
|
||||
void logEvent('Help Popover: Item clicked', {
|
||||
menuRoute: item.key,
|
||||
menuLabel: String(item.label),
|
||||
});
|
||||
@@ -890,7 +890,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
menuLabel = item.label;
|
||||
}
|
||||
|
||||
logEvent('Settings Popover: Item clicked', {
|
||||
void logEvent('Settings Popover: Item clicked', {
|
||||
menuRoute: item?.key,
|
||||
menuLabel,
|
||||
});
|
||||
@@ -927,7 +927,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
}
|
||||
break;
|
||||
case 'logout':
|
||||
Logout();
|
||||
void Logout();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
@@ -1081,7 +1081,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
<div
|
||||
className="nav-section-title-icon reorder"
|
||||
onClick={(): void => {
|
||||
logEvent('Sidebar V2: Manage shortcuts clicked', {});
|
||||
void logEvent('Sidebar V2: Manage shortcuts clicked', {});
|
||||
setIsReorderShortcutNavItemsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
@@ -1128,7 +1128,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
return;
|
||||
}
|
||||
const newCollapsedState = !isMoreMenuCollapsed;
|
||||
logEvent('Sidebar V2: More menu clicked', {
|
||||
void logEvent('Sidebar V2: More menu clicked', {
|
||||
action: isMoreMenuCollapsed ? 'expand' : 'collapse',
|
||||
});
|
||||
setIsMoreMenuCollapsed(newCollapsedState);
|
||||
@@ -1234,14 +1234,14 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
open={isReorderShortcutNavItemsModalOpen}
|
||||
closable
|
||||
onCancel={(): void => {
|
||||
logEvent('Sidebar V2: Manage shortcuts dismissed', {});
|
||||
void logEvent('Sidebar V2: Manage shortcuts dismissed', {});
|
||||
hideReorderShortcutNavItemsModal();
|
||||
}}
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={(): void => {
|
||||
logEvent('Sidebar V2: Manage shortcuts dismissed', {});
|
||||
void logEvent('Sidebar V2: Manage shortcuts dismissed', {});
|
||||
hideReorderShortcutNavItemsModal();
|
||||
}}
|
||||
className="periscope-btn cancel-btn secondary-btn"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { MenuProps } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
BarChart,
|
||||
@@ -35,15 +37,13 @@ import {
|
||||
Users,
|
||||
Binoculars,
|
||||
} from '@signozhq/icons';
|
||||
import { Style } from '@signozhq/design-tokens';
|
||||
import { MenuProps } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import {
|
||||
SecondaryMenuItemKey,
|
||||
SettingsNavSection,
|
||||
SidebarItem,
|
||||
} from './sideNav.types';
|
||||
import { Style } from '@signozhq/design-tokens';
|
||||
|
||||
export const getStartedMenuItem = {
|
||||
key: ROUTES.GET_STARTED,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Collapse, Input, Modal } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Collapse, Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import { Diamond } from '@signozhq/icons';
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Skeleton,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Input } from 'antd';
|
||||
// TODO(@signozhq/ui-input): migrate this styled(Input) once @signozhq/ui
|
||||
// Input supports `addonAfter` (the consumer renders `<InputComponent addonAfter="ms">`).
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Input } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const DurationText = styled.div`
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AutoComplete, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { AutoComplete } from 'antd';
|
||||
import getTagFilters from 'api/trace/getTagFilter';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AutoComplete, Input, Space } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { AutoComplete, Space } from 'antd';
|
||||
import getTagFilters from 'api/trace/getTagFilter';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChangeEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { ArrowLeft, Check, Loader, Plus, Search } from '@signozhq/icons';
|
||||
import { Button, Input, Spin } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Spin } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const InputComponent = styled(Input)`
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { Input, Select } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import './DropRateView.styles.scss';
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { ColorPicker, Input, Modal, Table, TableProps } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { ColorPicker, Modal, Table, TableProps } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
|
||||
@@ -321,7 +321,7 @@ function SettingsPage(): JSX.Element {
|
||||
isDisabled={false}
|
||||
showIcon={false}
|
||||
onClick={(event): void => {
|
||||
logEvent('Settings V2: Menu clicked', {
|
||||
void logEvent('Settings V2: Menu clicked', {
|
||||
menuLabel: item.label,
|
||||
menuRoute: item.key,
|
||||
});
|
||||
|
||||
@@ -119,6 +119,12 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.statusMessageBadge {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.traceId {
|
||||
color: var(--accent-primary);
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Checkbox, Input, Select, Skeleton } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Checkbox, Select, Skeleton } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import ExpandableValue from 'periscope/components/ExpandableValue';
|
||||
import { SpanV3 } from 'types/api/trace/getTraceV3';
|
||||
|
||||
import styles from './SpanDetailsPanel.module.scss';
|
||||
@@ -48,7 +49,15 @@ export const HIGHLIGHTED_OPTIONS: HighlightedOption[] = [
|
||||
label: 'STATUS MESSAGE',
|
||||
render: (span): ReactNode | null =>
|
||||
span.status_message ? (
|
||||
<Badge color="vanilla">{span.status_message}</Badge>
|
||||
<ExpandableValue value={span.status_message} title="Status message">
|
||||
<Badge
|
||||
color="vanilla"
|
||||
textEllipsis="end"
|
||||
className={styles.statusMessageBadge}
|
||||
>
|
||||
{span.status_message}
|
||||
</Badge>
|
||||
</ExpandableValue>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.traceOptionsDropdown {
|
||||
z-index: 1100;
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { Ellipsis } from '@signozhq/icons';
|
||||
|
||||
import { useTraceStore } from '../stores/traceStore';
|
||||
|
||||
import styles from './TraceOptionsMenu.module.scss';
|
||||
|
||||
interface TraceOptionsMenuProps {
|
||||
showTraceDetails: boolean;
|
||||
onToggleTraceDetails: () => void;
|
||||
@@ -82,7 +84,11 @@ function TraceOptionsMenu({
|
||||
]);
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items: menuItems }} align="start">
|
||||
<Dropdown
|
||||
menu={{ items: menuItems }}
|
||||
align="start"
|
||||
className={styles.traceOptionsDropdown}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChangeEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input, Spin } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Spin } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useRenameFunnel } from 'hooks/TracesFunnels/useFunnels';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChangeEvent } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Input, Popover, Tooltip } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Popover, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ArrowDownWideNarrow, Check, Plus, Search } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
.trigger {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
[data-truncated='true'] {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltipContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 480px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
margin: 0;
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: var(--font-family-mono, monospace);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.expandButton {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
max-width: 80vw;
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
.fullValue {
|
||||
margin: 0;
|
||||
max-height: 70vh;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: var(--font-family-mono, monospace);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import {
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipRoot,
|
||||
TooltipTrigger,
|
||||
} from '@signozhq/ui/tooltip';
|
||||
import { Fullscreen } from '@signozhq/icons';
|
||||
|
||||
import styles from './ExpandableValue.module.scss';
|
||||
|
||||
const DEFAULT_THRESHOLD = 100;
|
||||
const DEFAULT_DIALOG_TITLE = 'Value';
|
||||
|
||||
const DEFAULT_Z_INDEX = 1100;
|
||||
|
||||
interface ExpandableValueProps {
|
||||
value: string;
|
||||
title?: string;
|
||||
threshold?: number;
|
||||
zIndex?: number;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function ExpandableValue({
|
||||
value,
|
||||
title = DEFAULT_DIALOG_TITLE,
|
||||
threshold = DEFAULT_THRESHOLD,
|
||||
zIndex = DEFAULT_Z_INDEX,
|
||||
children,
|
||||
}: ExpandableValueProps): JSX.Element {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
if (value.length <= threshold) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipRoot>
|
||||
<TooltipTrigger asChild>
|
||||
<span className={styles.trigger}>{children}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className={styles.tooltipContent}
|
||||
side="top"
|
||||
style={{ zIndex }}
|
||||
>
|
||||
<pre className={styles.preview}>{value}</pre>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
prefix={<Fullscreen size={14} />}
|
||||
onClick={(): void => setIsDialogOpen(true)}
|
||||
className={styles.expandButton}
|
||||
>
|
||||
Expand
|
||||
</Button>
|
||||
</TooltipContent>
|
||||
</TooltipRoot>
|
||||
|
||||
<DialogWrapper
|
||||
title={title}
|
||||
open={isDialogOpen}
|
||||
onOpenChange={setIsDialogOpen}
|
||||
className={styles.dialog}
|
||||
style={{ zIndex }}
|
||||
>
|
||||
<pre className={styles.fullValue}>{value}</pre>
|
||||
</DialogWrapper>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExpandableValue;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ExpandableValue';
|
||||
@@ -13,8 +13,11 @@ import { useQuery } from 'react-query';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import { useGetHosts } from 'api/generated/services/zeus';
|
||||
import { useGetGlobalConfig } from 'api/generated/services/global';
|
||||
import { useGetMyUser } from 'api/generated/services/users';
|
||||
import listOrgPreferences from 'api/v1/org/preferences/list';
|
||||
import { clearAuthStorage } from 'utils/clearAuthStorage';
|
||||
import { getIsNoAuthMode, setNoAuthMode } from 'utils/noAuthMode';
|
||||
import listUserPreferences from 'api/v1/user/preferences/list';
|
||||
import getUserVersion from 'api/v1/version/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
@@ -70,11 +73,48 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(
|
||||
(): boolean => getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true',
|
||||
);
|
||||
const [isPreflightLoading, setIsPreflightLoading] = useState<boolean>(true);
|
||||
const [org, setOrg] = useState<Organization[] | null>(null);
|
||||
const [changelog, setChangelog] = useState<ChangelogSchema | null>(null);
|
||||
|
||||
const [showChangelogModal, setShowChangelogModal] = useState<boolean>(false);
|
||||
|
||||
// Pre-flight: discover auth mode from public global config.
|
||||
// On success: in impersonation mode → clear stale tokens, force isLoggedIn=true,
|
||||
// set noAuthMode singleton so the axios interceptor (outside React)
|
||||
// can skip the rotate-logout chain.
|
||||
// On failure: fail-safe to normal auth flow (treat as not no-auth).
|
||||
const { data: globalConfigData, isLoading: isFetchingGlobalConfig } =
|
||||
useGetGlobalConfig({
|
||||
query: {
|
||||
retry: 2,
|
||||
retryDelay: 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetchingGlobalConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const impersonationEnabled =
|
||||
globalConfigData?.data?.identN?.impersonation?.enabled === true;
|
||||
|
||||
if (impersonationEnabled) {
|
||||
clearAuthStorage();
|
||||
setDefaultUser(getUserDefaults());
|
||||
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
|
||||
setNoAuthMode(true);
|
||||
setIsLoggedIn(true);
|
||||
} else {
|
||||
setNoAuthMode(false);
|
||||
}
|
||||
|
||||
setIsPreflightLoading(false);
|
||||
}, [globalConfigData, isFetchingGlobalConfig]);
|
||||
|
||||
// fetcher for current user
|
||||
// user will only be fetched if the user id and token is present
|
||||
// if logged out and trying to hit any route none of these calls will trigger
|
||||
@@ -366,6 +406,9 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
|
||||
// global event listener for LOGOUT event to clean the app context state
|
||||
useGlobalEventListener('LOGOUT', () => {
|
||||
if (getIsNoAuthMode()) {
|
||||
return;
|
||||
} // logout is meaningless in no-auth; defensively no-op
|
||||
setIsLoggedIn(false);
|
||||
setDefaultUser(getUserDefaults());
|
||||
setActiveLicense(null);
|
||||
@@ -385,6 +428,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
orgPreferences,
|
||||
hostsData,
|
||||
isLoggedIn,
|
||||
isPreflightLoading,
|
||||
org,
|
||||
isFetchingUser,
|
||||
isFetchingActiveLicense,
|
||||
@@ -425,6 +469,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
isLoggedIn,
|
||||
hostsData,
|
||||
hostsFetchError,
|
||||
isPreflightLoading,
|
||||
org,
|
||||
orgPreferences,
|
||||
activeLicenseRefetch,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ReactElement } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import { getIsNoAuthMode, setNoAuthMode } from 'utils/noAuthMode';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { SINGLE_FLIGHT_WAIT_TIME_MS } from 'hooks/useAuthZ/constants';
|
||||
import { server } from 'mocks-server/server';
|
||||
@@ -13,6 +14,7 @@ import { AppProvider, useAppContext } from '../App';
|
||||
|
||||
const MY_USER_URL = 'http://localhost/api/v2/users/me';
|
||||
const MY_ORG_URL = 'http://localhost/api/v2/orgs/me';
|
||||
const GLOBAL_CONFIG_URL = 'http://localhost/api/v1/global/config';
|
||||
|
||||
jest.mock('constants/env', () => ({
|
||||
ENVIRONMENT: { baseURL: 'http://localhost', wsURL: '' },
|
||||
@@ -336,3 +338,126 @@ describe('AppProvider when authz/check fails', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppProvider no-auth preflight', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setNoAuthMode(false);
|
||||
});
|
||||
|
||||
it('sets noAuthMode singleton when impersonation is enabled', async () => {
|
||||
server.use(
|
||||
rest.get(GLOBAL_CONFIG_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: { identN: { impersonation: { enabled: true } } },
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.isPreflightLoading).toBe(false);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
expect(getIsNoAuthMode()).toBe(true);
|
||||
});
|
||||
|
||||
it('leaves noAuthMode singleton false when impersonation is disabled', async () => {
|
||||
server.use(
|
||||
rest.get(GLOBAL_CONFIG_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: { identN: { impersonation: { enabled: false } } },
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.isPreflightLoading).toBe(false);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
expect(getIsNoAuthMode()).toBe(false);
|
||||
});
|
||||
|
||||
it('clears stale auth tokens from localStorage and resets in-memory JWT state when impersonation is enabled', async () => {
|
||||
setLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN, 'stale-access-token');
|
||||
setLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN, 'stale-refresh-token');
|
||||
setLocalStorageApi(LOCALSTORAGE.LOGGED_IN_USER_EMAIL, 'old@example.com');
|
||||
setLocalStorageApi(LOCALSTORAGE.LOGGED_IN_USER_NAME, 'Old Name');
|
||||
|
||||
server.use(
|
||||
rest.get(GLOBAL_CONFIG_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: { identN: { impersonation: { enabled: true } } },
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.isPreflightLoading).toBe(false);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
// localStorage cleared
|
||||
expect(localStorage.getItem(LOCALSTORAGE.AUTH_TOKEN)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.REFRESH_AUTH_TOKEN)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.LOGGED_IN_USER_EMAIL)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.LOGGED_IN_USER_NAME)).toBeNull();
|
||||
|
||||
// in-memory JWTs reset so stale tokens don't linger in context or React Query keys
|
||||
expect(result.current.user.accessJwt).toBe('');
|
||||
expect(result.current.user.refreshJwt).toBe('');
|
||||
});
|
||||
|
||||
it('transitions isPreflightLoading from true to false once preflight resolves', async () => {
|
||||
server.use(
|
||||
rest.get(GLOBAL_CONFIG_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
data: { identN: { impersonation: { enabled: false } } },
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
expect(result.current.isPreflightLoading).toBe(true);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.isPreflightLoading).toBe(false);
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface IAppContext {
|
||||
userPreferences: UserPreference[] | null;
|
||||
hostsData: GetHosts200 | null;
|
||||
isLoggedIn: boolean;
|
||||
isPreflightLoading: boolean;
|
||||
org: Organization[] | null;
|
||||
isFetchingUser: boolean;
|
||||
isFetchingActiveLicense: boolean;
|
||||
|
||||
@@ -243,6 +243,7 @@ export function getAppContextMock(
|
||||
isFetchingOrgPreferences: false,
|
||||
orgPreferencesFetchError: null,
|
||||
isLoggedIn: true,
|
||||
isPreflightLoading: false,
|
||||
showChangelogModal: false,
|
||||
updateUser: jest.fn(),
|
||||
updateOrg: jest.fn(),
|
||||
|
||||
39
frontend/src/utils/__tests__/clearAuthStorage.test.ts
Normal file
39
frontend/src/utils/__tests__/clearAuthStorage.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
import { clearAuthStorage } from '../clearAuthStorage';
|
||||
|
||||
describe('clearAuthStorage', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('removes all auth-related localStorage keys', () => {
|
||||
localStorage.setItem(LOCALSTORAGE.AUTH_TOKEN, 'access');
|
||||
localStorage.setItem(LOCALSTORAGE.REFRESH_AUTH_TOKEN, 'refresh');
|
||||
localStorage.setItem(LOCALSTORAGE.IS_LOGGED_IN, 'true');
|
||||
localStorage.setItem(LOCALSTORAGE.LOGGED_IN_USER_EMAIL, 'old@example.com');
|
||||
localStorage.setItem(LOCALSTORAGE.LOGGED_IN_USER_NAME, 'old');
|
||||
localStorage.setItem(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
|
||||
localStorage.setItem(LOCALSTORAGE.USER_ID, 'abc');
|
||||
|
||||
clearAuthStorage();
|
||||
|
||||
expect(localStorage.getItem(LOCALSTORAGE.AUTH_TOKEN)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.REFRESH_AUTH_TOKEN)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.IS_LOGGED_IN)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.LOGGED_IN_USER_EMAIL)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.LOGGED_IN_USER_NAME)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.IS_IDENTIFIED_USER)).toBeNull();
|
||||
expect(localStorage.getItem(LOCALSTORAGE.USER_ID)).toBeNull();
|
||||
});
|
||||
|
||||
it('preserves non-auth localStorage keys', () => {
|
||||
localStorage.setItem(LOCALSTORAGE.THEME, 'dark');
|
||||
localStorage.setItem(LOCALSTORAGE.AUTH_TOKEN, 'access');
|
||||
|
||||
clearAuthStorage();
|
||||
|
||||
expect(localStorage.getItem(LOCALSTORAGE.THEME)).toBe('dark');
|
||||
expect(localStorage.getItem(LOCALSTORAGE.AUTH_TOKEN)).toBeNull();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user