Compare commits

..

8 Commits

Author SHA1 Message Date
swapnil-signoz
a09dc325de Merge branch 'main' into refactor/cloud-integration-impl-store 2026-03-02 16:39:20 +05:30
swapnil-signoz
379b4f7fc4 refactor: removing interface check 2026-03-02 14:50:37 +05:30
swapnil-signoz
5e536ae077 Merge branch 'refactor/cloud-integration-types' into refactor/cloud-integration-impl-store 2026-03-02 14:49:35 +05:30
swapnil-signoz
234585e642 Merge branch 'main' into refactor/cloud-integration-types 2026-03-02 14:49:19 +05:30
swapnil-signoz
2cc14f1ad4 Merge branch 'main' into refactor/cloud-integration-impl-store 2026-03-02 14:49:00 +05:30
swapnil-signoz
dc4ed4d239 feat: adding sql store implementation 2026-03-02 14:44:56 +05:30
swapnil-signoz
7281c36873 refactor: store interfaces to use local types and error 2026-03-02 13:27:46 +05:30
swapnil-signoz
40288776e8 feat: adding cloud integration type for refactor 2026-02-28 16:59:14 +05:30
273 changed files with 9363 additions and 17474 deletions

View File

@@ -58,19 +58,19 @@ jobs:
run: |
mkdir -p frontend
echo 'CI=1' > frontend/.env
echo 'VITE_INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' >> frontend/.env
echo 'VITE_SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> frontend/.env
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
echo 'VITE_TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'VITE_POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
echo 'VITE_PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
echo 'VITE_APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'VITE_DOCS_BASE_URL="https://signoz.io"' >> frontend/.env
echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' >> frontend/.env
echo 'SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> frontend/.env
echo 'SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
echo 'SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
echo 'SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'DOCS_BASE_URL="https://signoz.io"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:

View File

@@ -64,12 +64,12 @@ jobs:
run: |
mkdir -p frontend
echo 'CI=1' > frontend/.env
echo 'VITE_TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'VITE_PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
echo 'VITE_APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'VITE_DOCS_BASE_URL="https://staging.signoz.io"' >> frontend/.env
echo 'TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
echo 'APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
echo 'PYLON_IDENTITY_SECRET="${{ secrets.NP_PYLON_IDENTITY_SECRET }}"' >> frontend/.env
echo 'DOCS_BASE_URL="https://staging.signoz.io"' >> frontend/.env
- name: cache-dotenv
uses: actions/cache@v4
with:

View File

@@ -24,19 +24,19 @@ jobs:
- name: dotenv-frontend
working-directory: frontend
run: |
echo 'VITE_INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > .env
echo 'VITE_SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> .env
echo 'VITE_SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> .env
echo 'VITE_SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> .env
echo 'VITE_SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> .env
echo 'VITE_SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> .env
echo 'VITE_TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> .env
echo 'VITE_TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> .env
echo 'VITE_POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
echo 'VITE_PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
echo 'VITE_APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
echo 'VITE_PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
echo 'VITE_DOCS_BASE_URL="https://signoz.io"' >> .env
echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > .env
echo 'SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> .env
echo 'SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> .env
echo 'SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> .env
echo 'SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> .env
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> .env
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> .env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> .env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
echo 'PYLON_IDENTITY_SECRET="${{ secrets.PYLON_IDENTITY_SECRET }}"' >> .env
echo 'DOCS_BASE_URL="https://signoz.io"' >> .env
- name: node-setup
uses: actions/setup-node@v5
with:

View File

@@ -103,9 +103,8 @@ jobs:
make py-test-setup
- name: Generate permissions.type.ts
working-directory: ./frontend
run: |
yarn generate:permissions-type
node frontend/scripts/generate-permissions-type.js
- name: Teardown test environment
if: always()

View File

@@ -238,4 +238,4 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
.PHONY: gen-mocks
gen-mocks:
@echo ">> Generating mocks"
@mockery --config .mockery.yml
@mockery --config .mockery.yml

View File

@@ -1763,140 +1763,6 @@ components:
- type
- orgId
type: object
RulestatehistorytypesAlertState:
enum:
- inactive
- pending
- recovering
- firing
- nodata
- disabled
type: string
RulestatehistorytypesRuleStateHistoryContributorResponse:
properties:
count:
minimum: 0
type: integer
fingerprint:
minimum: 0
type: integer
labels:
items:
$ref: '#/components/schemas/Querybuildertypesv5Label'
nullable: true
type: array
relatedLogsLink:
type: string
relatedTracesLink:
type: string
required:
- fingerprint
- labels
- count
type: object
RulestatehistorytypesRuleStateHistoryResponseItem:
properties:
fingerprint:
minimum: 0
type: integer
labels:
items:
$ref: '#/components/schemas/Querybuildertypesv5Label'
nullable: true
type: array
overallState:
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
overallStateChanged:
type: boolean
ruleID:
type: string
ruleName:
type: string
state:
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
stateChanged:
type: boolean
unixMilli:
format: int64
type: integer
value:
format: double
type: number
required:
- ruleID
- ruleName
- overallState
- overallStateChanged
- state
- stateChanged
- unixMilli
- labels
- fingerprint
- value
type: object
RulestatehistorytypesRuleStateTimelineResponse:
properties:
items:
items:
$ref: '#/components/schemas/RulestatehistorytypesRuleStateHistoryResponseItem'
nullable: true
type: array
nextCursor:
type: string
total:
minimum: 0
type: integer
required:
- items
- total
type: object
RulestatehistorytypesRuleStateWindow:
properties:
end:
format: int64
type: integer
start:
format: int64
type: integer
state:
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
required:
- state
- start
- end
type: object
RulestatehistorytypesStats:
properties:
currentAvgResolutionTime:
format: double
type: number
currentAvgResolutionTimeSeries:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
currentTriggersSeries:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
pastAvgResolutionTime:
format: double
type: number
pastAvgResolutionTimeSeries:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
pastTriggersSeries:
$ref: '#/components/schemas/Querybuildertypesv5TimeSeries'
totalCurrentTriggers:
minimum: 0
type: integer
totalPastTriggers:
minimum: 0
type: integer
required:
- totalCurrentTriggers
- totalPastTriggers
- currentTriggersSeries
- pastTriggersSeries
- currentAvgResolutionTime
- pastAvgResolutionTime
- currentAvgResolutionTimeSeries
- pastAvgResolutionTimeSeries
type: object
ServiceaccounttypesFactorAPIKey:
properties:
createdAt:
@@ -6301,10 +6167,6 @@ paths:
name: searchText
schema:
type: string
- in: query
name: source
schema:
type: string
responses:
"200":
content:
@@ -6952,518 +6814,6 @@ paths:
summary: Update my organization
tags:
- orgs
/api/v2/rules/{id}/history/filter_keys:
get:
deprecated: false
description: Returns distinct label keys from rule history entries for the selected
range.
operationId: GetRuleHistoryFilterKeys
parameters:
- in: query
name: signal
schema:
$ref: '#/components/schemas/TelemetrytypesSignal'
- in: query
name: source
schema:
$ref: '#/components/schemas/TelemetrytypesSource'
- in: query
name: limit
schema:
type: integer
- in: query
name: startUnixMilli
schema:
format: int64
type: integer
- in: query
name: endUnixMilli
schema:
format: int64
type: integer
- in: query
name: fieldContext
schema:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
- in: query
name: fieldDataType
schema:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
- in: query
name: metricName
schema:
type: string
- in: query
name: searchText
schema:
type: string
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TelemetrytypesGettableFieldKeys'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get rule history filter keys
tags:
- rules
/api/v2/rules/{id}/history/filter_values:
get:
deprecated: false
description: Returns distinct label values for a given key from rule history
entries.
operationId: GetRuleHistoryFilterValues
parameters:
- in: query
name: signal
schema:
$ref: '#/components/schemas/TelemetrytypesSignal'
- in: query
name: source
schema:
$ref: '#/components/schemas/TelemetrytypesSource'
- in: query
name: limit
schema:
type: integer
- in: query
name: startUnixMilli
schema:
format: int64
type: integer
- in: query
name: endUnixMilli
schema:
format: int64
type: integer
- in: query
name: fieldContext
schema:
$ref: '#/components/schemas/TelemetrytypesFieldContext'
- in: query
name: fieldDataType
schema:
$ref: '#/components/schemas/TelemetrytypesFieldDataType'
- in: query
name: metricName
schema:
type: string
- in: query
name: searchText
schema:
type: string
- in: query
name: name
schema:
type: string
- in: query
name: existingQuery
schema:
type: string
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TelemetrytypesGettableFieldValues'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get rule history filter values
tags:
- rules
/api/v2/rules/{id}/history/overall_status:
get:
deprecated: false
description: Returns overall firing/inactive intervals for a rule in the selected
time range.
operationId: GetRuleHistoryOverallStatus
parameters:
- in: query
name: start
required: true
schema:
format: int64
type: integer
- in: query
name: end
required: true
schema:
format: int64
type: integer
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/RulestatehistorytypesRuleStateWindow'
nullable: true
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get rule overall status timeline
tags:
- rules
/api/v2/rules/{id}/history/stats:
get:
deprecated: false
description: Returns trigger and resolution statistics for a rule in the selected
time range.
operationId: GetRuleHistoryStats
parameters:
- in: query
name: start
required: true
schema:
format: int64
type: integer
- in: query
name: end
required: true
schema:
format: int64
type: integer
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RulestatehistorytypesStats'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get rule history stats
tags:
- rules
/api/v2/rules/{id}/history/timeline:
get:
deprecated: false
description: Returns paginated timeline entries for rule state transitions.
operationId: GetRuleHistoryTimeline
parameters:
- in: query
name: start
required: true
schema:
format: int64
type: integer
- in: query
name: end
required: true
schema:
format: int64
type: integer
- in: query
name: state
schema:
$ref: '#/components/schemas/RulestatehistorytypesAlertState'
- in: query
name: filterExpression
schema:
type: string
- in: query
name: limit
schema:
format: int64
type: integer
- in: query
name: order
schema:
$ref: '#/components/schemas/Querybuildertypesv5OrderDirection'
- in: query
name: cursor
schema:
type: string
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/RulestatehistorytypesRuleStateTimelineResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get rule history timeline
tags:
- rules
/api/v2/rules/{id}/history/top_contributors:
get:
deprecated: false
description: Returns top label combinations contributing to rule firing in the
selected time range.
operationId: GetRuleHistoryTopContributors
parameters:
- in: query
name: start
required: true
schema:
format: int64
type: integer
- in: query
name: end
required: true
schema:
format: int64
type: integer
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/RulestatehistorytypesRuleStateHistoryContributorResponse'
nullable: true
type: array
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get top contributors to rule firing
tags:
- rules
/api/v2/sessions:
delete:
deprecated: false

View File

@@ -25,7 +25,6 @@ import (
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/signoz"
@@ -103,7 +102,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
signoz.TelemetryMetadataStore,
signoz.Prometheus,
signoz.Modules.OrgGetter,
signoz.Modules.RuleStateHistory,
signoz.Querier,
signoz.Instrumentation.ToProviderSettings(),
signoz.QueryParser,
@@ -351,30 +349,29 @@ func (s *Server) Stop(ctx context.Context) error {
return nil
}
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, ruleStateHistoryModule rulestatehistory.Module, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
// create manager opts
managerOpts := &baserules.ManagerOptions{
TelemetryStore: telemetryStore,
MetadataStore: metadataStore,
Prometheus: prometheus,
Context: context.Background(),
Logger: zap.L(),
Reader: ch,
Querier: querier,
SLogger: providerSettings.Logger,
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
PrepareTestRuleFunc: rules.TestNotification,
Alertmanager: alertmanager,
OrgGetter: orgGetter,
RuleStore: ruleStore,
MaintenanceStore: maintenanceStore,
SqlStore: sqlstore,
QueryParser: queryParser,
RuleStateHistoryModule: ruleStateHistoryModule,
TelemetryStore: telemetryStore,
MetadataStore: metadataStore,
Prometheus: prometheus,
Context: context.Background(),
Logger: zap.L(),
Reader: ch,
Querier: querier,
SLogger: providerSettings.Logger,
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
PrepareTestRuleFunc: rules.TestNotification,
Alertmanager: alertmanager,
OrgGetter: orgGetter,
RuleStore: ruleStore,
MaintenanceStore: maintenanceStore,
SqlStore: sqlstore,
QueryParser: queryParser,
}
// create Manager

View File

@@ -26,7 +26,6 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
}
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule
tr, err := baserules.NewThresholdRule(
@@ -40,7 +39,6 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
@@ -65,7 +63,6 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
@@ -91,7 +88,6 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
baserules.WithSQLStore(opts.SQLStore),
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
return task, err

16
frontend/.babelrc Normal file
View File

@@ -0,0 +1,16 @@
{
"presets": [
"@babel/preset-env",
["@babel/preset-react", { "runtime": "automatic" }],
"@babel/preset-typescript"
],
"plugins": [
"react-hot-loader/babel",
"@babel/plugin-proposal-class-properties"
],
"env": {
"production": {
"presets": ["minify"]
}
}
}

View File

@@ -11,6 +11,7 @@ module.exports = {
browser: true,
es2021: true,
node: true,
'jest/globals': true,
},
extends: [
'eslint:recommended',
@@ -24,7 +25,6 @@ module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
ecmaFeatures: {
jsx: true,
},
@@ -37,7 +37,7 @@ module.exports = {
'simple-import-sort', // Auto-sort imports
'react-hooks', // React Hooks rules
'prettier', // Code formatting
// 'jest', // TODO: Wait support on Biome to enable again
'jest', // Jest test rules
'jsx-a11y', // Accessibility rules
'import', // Import/export linting
'sonarjs', // Code quality/complexity

View File

@@ -1 +1 @@
22
16.15.0

View File

@@ -1,4 +0,0 @@
export const ENVIRONMENT = {
baseURL: process.env.VITE_FRONTEND_API_ENDPOINT || '',
wsURL: process.env.VITE_WEBSOCKET_API_ENDPOINT || '',
};

View File

@@ -1,20 +0,0 @@
module.exports = {
presets: [
['@babel/preset-env', { modules: 'auto' }],
['@babel/preset-react', { runtime: 'automatic' }],
['@babel/preset-typescript'],
],
plugins: ['@babel/plugin-proposal-class-properties'],
env: {
test: {
presets: [
[
'@babel/preset-env',
{ modules: 'commonjs', targets: { node: 'current' } },
],
['@babel/preset-react', { runtime: 'automatic' }],
['@babel/preset-typescript'],
],
},
},
};

6
frontend/babel.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};

View File

@@ -0,0 +1,8 @@
{
"files": [
{
"path": "./build/**.js",
"maxSize": "1.2MB"
}
]
}

View File

@@ -1,8 +1,8 @@
NODE_ENV="development"
BUNDLE_ANALYSER="true"
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080/"
VITE_PYLON_APP_ID="pylon-app-id"
VITE_APPCUES_APP_ID="appcess-app-id"
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"
FRONTEND_API_ENDPOINT="http://localhost:8080/"
PYLON_APP_ID="pylon-app-id"
APPCUES_APP_ID="appcess-app-id"
PYLON_IDENTITY_SECRET="pylon-identity-secret"
CI="1"
CI="1"

View File

@@ -17,53 +17,29 @@ const config: Config.InitialOptions = {
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^constants/env$': '<rootDir>/__mocks__/env.ts',
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',
'^@signozhq/icons$':
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
'^react-syntax-highlighter/dist/esm/(.*)$':
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',
'^@signozhq/sonner$':
'<rootDir>/node_modules/@signozhq/sonner/dist/sonner.js',
'^@signozhq/button$':
'<rootDir>/node_modules/@signozhq/button/dist/button.js',
'^@signozhq/calendar$':
'<rootDir>/node_modules/@signozhq/calendar/dist/calendar.js',
'^@signozhq/badge': '<rootDir>/node_modules/@signozhq/badge/dist/badge.js',
'^@signozhq/checkbox':
'<rootDir>/node_modules/@signozhq/checkbox/dist/checkbox.js',
'^@signozhq/switch': '<rootDir>/node_modules/@signozhq/switch/dist/switch.js',
'^@signozhq/callout':
'<rootDir>/node_modules/@signozhq/callout/dist/callout.js',
'^@signozhq/combobox':
'<rootDir>/node_modules/@signozhq/combobox/dist/combobox.js',
'^@signozhq/input': '<rootDir>/node_modules/@signozhq/input/dist/input.js',
'^@signozhq/command':
'<rootDir>/node_modules/@signozhq/command/dist/command.js',
'^@signozhq/radio-group':
'<rootDir>/node_modules/@signozhq/radio-group/dist/radio-group.js',
'^@signozhq/toggle-group$':
'<rootDir>/node_modules/@signozhq/toggle-group/dist/toggle-group.js',
'^@signozhq/dialog$':
'<rootDir>/node_modules/@signozhq/dialog/dist/dialog.js',
},
extensionsToTreatAsEsm: ['.ts'],
globals: {
extensionsToTreatAsEsm: ['.ts'],
'ts-jest': {
useESM: true,
isolatedModules: true,
tsconfig: '<rootDir>/tsconfig.jest.json',
},
},
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
preset: 'ts-jest/presets/js-with-ts-esm',
transform: {
'^.+\\.(ts|tsx)?$': [
'ts-jest',
{
useESM: true,
tsconfig: '<rootDir>/tsconfig.jest.json',
},
],
'^.+\\.(ts|tsx)?$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],
moduleDirectories: ['node_modules', 'src'],
testEnvironment: 'jest-environment-jsdom',

View File

@@ -1,285 +1,293 @@
{
"name": "frontend",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"i18n:generate-hash": "node ./i18-generate-hash.cjs",
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"prettify": "prettier --write .",
"fmt": "prettier --check .",
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure) && node scripts/update-registry.cjs",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:permissions-type": "node scripts/generate-permissions-type.cjs"
},
"engines": {
"node": ">=22.0.0"
},
"author": "",
"license": "ISC",
"dependencies": {
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/lang-javascript": "6.2.3",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
"@grafana/data": "^11.2.3",
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@monaco-editor/react": "^4.3.1",
"@playwright/test": "1.55.1",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
"@sentry/vite-plugin": "2.22.6",
"@signozhq/badge": "0.0.2",
"@signozhq/button": "0.0.2",
"@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2",
"@signozhq/checkbox": "0.0.2",
"@signozhq/combobox": "0.0.2",
"@signozhq/command": "0.0.0",
"@signozhq/design-tokens": "2.1.1",
"@signozhq/dialog": "^0.0.2",
"@signozhq/icons": "0.1.0",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/radio-group": "0.0.2",
"@signozhq/resizable": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/switch": "0.0.2",
"@signozhq/table": "0.3.7",
"@signozhq/toggle-group": "^0.0.1",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/hierarchy": "3.12.0",
"@visx/shape": "3.5.0",
"@visx/tooltip": "3.3.0",
"@vitejs/plugin-react": "5.1.4",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"antlr4": "4.13.2",
"axios": "1.12.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",
"babel-plugin-named-asset-import": "^0.3.7",
"babel-preset-minify": "^0.5.1",
"babel-preset-react-app": "^10.0.1",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0",
"classnames": "2.3.2",
"color": "^4.2.1",
"color-alpha": "2.0.0",
"cross-env": "^7.0.3",
"crypto-js": "4.2.0",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.2.4",
"dotenv": "8.2.0",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"fontfaceobserver": "2.3.0",
"history": "4.10.1",
"http-proxy-middleware": "3.0.5",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"immer": "11.1.3",
"jest": "30.2.0",
"js-base64": "^3.7.2",
"lodash-es": "^4.17.21",
"lucide-react": "0.498.0",
"mini-css-extract-plugin": "2.4.5",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.298.0",
"rc-tween-one": "3.0.6",
"react": "18.2.0",
"react-addons-update": "15.6.3",
"react-beautiful-dnd": "13.1.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.2.0",
"react-drag-listview": "2.0.0",
"react-error-boundary": "4.0.11",
"react-force-graph-2d": "^1.29.1",
"react-full-screen": "1.1.1",
"react-grid-layout": "^1.3.4",
"react-helmet-async": "1.3.0",
"react-i18next": "^11.16.1",
"react-lottie": "1.2.10",
"react-markdown": "8.0.7",
"react-query": "3.39.3",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "6.27.0",
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
"rollup-plugin-visualizer": "7.0.0",
"rrule": "2.8.1",
"stream": "^0.0.2",
"styled-components": "^5.3.11",
"timestamp-nano": "^1.0.0",
"ts-node": "^10.2.1",
"typescript": "5.9.3",
"uplot": "1.6.31",
"uuid": "^8.3.2",
"vite": "npm:rolldown-vite@7.3.1",
"vite-plugin-html": "3.2.2",
"web-vitals": "^0.2.4",
"xstate": "^4.31.0",
"zustand": "5.0.11"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.22.11",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-syntax-jsx": "^7.12.13",
"@babel/preset-env": "^7.22.14",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.21.4",
"@commitlint/cli": "^20.4.2",
"@commitlint/config-conventional": "^20.4.2",
"@faker-js/faker": "9.3.0",
"@jest/globals": "30.2.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
"@types/crypto-js": "4.2.2",
"@types/dompurify": "^2.4.0",
"@types/event-source-polyfill": "^1.0.0",
"@types/fontfaceobserver": "2.1.0",
"@types/jest": "30.0.0",
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
"@types/papaparse": "5.3.7",
"@types/react": "18.0.26",
"@types/react-addons-update": "0.14.21",
"@types/react-beautiful-dnd": "13.1.8",
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-helmet-async": "1.0.3",
"@types/react-lottie": "1.2.10",
"@types/react-redux": "^7.1.11",
"@types/react-resizable": "3.0.3",
"@types/react-router-dom": "^5.1.6",
"@types/react-syntax-highlighter": "15.5.13",
"@types/redux-mock-store": "1.0.4",
"@types/styled-components": "^5.1.4",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"autoprefixer": "10.4.19",
"babel-plugin-styled-components": "^1.12.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jest": "^29.15.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.12.0",
"husky": "^7.0.4",
"imagemin": "^8.0.1",
"imagemin-svgo": "^10.0.1",
"is-ci": "^3.0.1",
"jest-environment-jsdom": "29.7.0",
"jest-environment-node": "29.7.0",
"jest-styled-components": "^7.2.0",
"lint-staged": "^12.5.0",
"msw": "1.3.2",
"npm-run-all": "latest",
"orval": "7.18.0",
"portfinder-sync": "^0.0.2",
"postcss": "8.5.6",
"prettier": "2.2.1",
"prop-types": "15.8.1",
"react-hooks-testing-library": "0.6.0",
"react-resizable": "3.0.4",
"redux-mock-store": "1.5.4",
"sass": "1.97.3",
"sharp": "0.34.5",
"svgo": "4.0.0",
"ts-api-utils": "2.4.0",
"ts-jest": "29.4.6",
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.2.0",
"vite-plugin-checker": "0.12.0",
"vite-plugin-compression": "0.5.1",
"vite-plugin-image-optimizer": "2.0.3",
"vite-tsconfig-paths": "6.1.1"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
"eslint --fix",
"sh scripts/typecheck-staged.sh"
]
},
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"debug": "4.3.4",
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.5",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.4",
"brace-expansion": "^2.0.2",
"on-headers": "^1.1.0",
"tmp": "0.2.4",
"vite": "npm:rolldown-vite@7.3.1"
}
}
"name": "frontend",
"version": "1.0.0",
"description": "",
"main": "webpack.config.js",
"scripts": {
"i18n:generate-hash": "node ./i18-generate-hash.js",
"dev": "cross-env NODE_ENV=development webpack serve --progress",
"build": "webpack --config=webpack.config.prod.js --progress",
"prettify": "prettier --write .",
"fmt": "prettier --check .",
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "yarn i18n:generate-hash && (is-ci || yarn husky:configure) && node scripts/update-registry.js",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1",
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:permissions-type": "node scripts/generate-permissions-type.js"
},
"engines": {
"node": ">=16.15.0"
},
"author": "",
"license": "ISC",
"dependencies": {
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/lang-javascript": "6.2.3",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@dnd-kit/utilities": "3.2.2",
"@grafana/data": "^11.2.3",
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@monaco-editor/react": "^4.3.1",
"@playwright/test": "1.55.1",
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
"@sentry/webpack-plugin": "2.22.6",
"@signozhq/badge": "0.0.2",
"@signozhq/button": "0.0.2",
"@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2",
"@signozhq/checkbox": "0.0.2",
"@signozhq/combobox": "0.0.2",
"@signozhq/command": "0.0.0",
"@signozhq/design-tokens": "2.1.1",
"@signozhq/icons": "0.1.0",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/radio-group": "0.0.2",
"@signozhq/resizable": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/switch": "0.0.2",
"@signozhq/table": "0.3.7",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
"@uiw/codemirror-theme-github": "4.24.1",
"@uiw/react-codemirror": "4.23.10",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/hierarchy": "3.12.0",
"@visx/shape": "3.5.0",
"@visx/tooltip": "3.3.0",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"antlr4": "4.13.2",
"axios": "1.12.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",
"babel-plugin-named-asset-import": "^0.3.7",
"babel-preset-minify": "^0.5.1",
"babel-preset-react-app": "^10.0.1",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0",
"classnames": "2.3.2",
"color": "^4.2.1",
"color-alpha": "1.1.3",
"cross-env": "^7.0.3",
"crypto-js": "4.2.0",
"css-loader": "5.0.0",
"css-minimizer-webpack-plugin": "5.0.1",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.2.4",
"dotenv": "8.2.0",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"file-loader": "6.1.1",
"fontfaceobserver": "2.3.0",
"history": "4.10.1",
"html-webpack-plugin": "5.5.0",
"http-proxy-middleware": "3.0.5",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"immer": "11.1.3",
"jest": "^27.5.1",
"js-base64": "^3.7.2",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
"lucide-react": "0.498.0",
"mini-css-extract-plugin": "2.4.5",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.298.0",
"rc-tween-one": "3.0.6",
"react": "18.2.0",
"react-addons-update": "15.6.3",
"react-beautiful-dnd": "13.1.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.2.0",
"react-drag-listview": "2.0.0",
"react-error-boundary": "4.0.11",
"react-force-graph-2d": "^1.29.1",
"react-full-screen": "1.1.1",
"react-grid-layout": "^1.3.4",
"react-helmet-async": "1.3.0",
"react-i18next": "^11.16.1",
"react-lottie": "1.2.10",
"react-markdown": "8.0.7",
"react-query": "3.39.3",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "6.27.0",
"react-syntax-highlighter": "15.5.0",
"react-use": "^17.3.2",
"react-virtuoso": "4.0.3",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
"rrule": "2.8.1",
"stream": "^0.0.2",
"style-loader": "1.3.0",
"styled-components": "^5.3.11",
"terser-webpack-plugin": "^5.2.5",
"timestamp-nano": "^1.0.0",
"ts-node": "^10.2.1",
"tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "^4.0.5",
"uplot": "1.6.31",
"uuid": "^8.3.2",
"web-vitals": "^0.2.4",
"webpack": "5.94.0",
"webpack-dev-server": "^5.2.1",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0",
"zustand": "5.0.11"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/core": "^7.22.11",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-syntax-jsx": "^7.12.13",
"@babel/preset-env": "^7.22.14",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.21.4",
"@commitlint/cli": "^16.3.0",
"@commitlint/config-conventional": "^16.2.4",
"@faker-js/faker": "9.3.0",
"@jest/globals": "^27.5.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@types/color": "^3.0.3",
"@types/compression-webpack-plugin": "^9.0.0",
"@types/copy-webpack-plugin": "^8.0.1",
"@types/crypto-js": "4.2.2",
"@types/dompurify": "^2.4.0",
"@types/event-source-polyfill": "^1.0.0",
"@types/fontfaceobserver": "2.1.0",
"@types/jest": "^27.5.1",
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
"@types/papaparse": "5.3.7",
"@types/react": "18.0.26",
"@types/react-addons-update": "0.14.21",
"@types/react-beautiful-dnd": "13.1.8",
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-helmet-async": "1.0.3",
"@types/react-lottie": "1.2.10",
"@types/react-redux": "^7.1.11",
"@types/react-resizable": "3.0.3",
"@types/react-router-dom": "^5.1.6",
"@types/react-syntax-highlighter": "15.5.13",
"@types/redux-mock-store": "1.0.4",
"@types/styled-components": "^5.1.4",
"@types/uuid": "^8.3.1",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^4.7.2",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"autoprefixer": "10.4.19",
"babel-plugin-styled-components": "^1.12.0",
"compression-webpack-plugin": "9.0.0",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jest": "^26.9.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.12.0",
"husky": "^7.0.4",
"image-minimizer-webpack-plugin": "^4.0.0",
"imagemin": "^8.0.1",
"imagemin-svgo": "^10.0.1",
"is-ci": "^3.0.1",
"jest-styled-components": "^7.0.8",
"lint-staged": "^12.5.0",
"msw": "1.3.2",
"npm-run-all": "latest",
"orval": "7.18.0",
"portfinder-sync": "^0.0.2",
"postcss": "8.4.38",
"prettier": "2.2.1",
"prop-types": "15.8.1",
"raw-loader": "4.0.2",
"react-hooks-testing-library": "0.6.0",
"react-hot-loader": "^4.13.0",
"react-resizable": "3.0.4",
"redux-mock-store": "1.5.4",
"sass": "1.66.1",
"sass-loader": "13.3.2",
"sharp": "^0.33.4",
"ts-jest": "^27.1.5",
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.2.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^5.1.4"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
"eslint --fix",
"sh scripts/typecheck-staged.sh"
]
},
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"debug": "4.3.4",
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.5",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.4",
"brace-expansion": "^2.0.2",
"on-headers": "^1.1.0",
"tmp": "0.2.4"
}
}

View File

@@ -13,6 +13,5 @@
"pipelines": "Pipelines",
"archives": "Archives",
"logs_to_metrics": "Logs To Metrics",
"roles": "Roles",
"role_details": "Role Details"
"roles": "Roles"
}

View File

@@ -13,6 +13,5 @@
"pipelines": "Pipelines",
"archives": "Archives",
"logs_to_metrics": "Logs To Metrics",
"roles": "Roles",
"role_details": "Role Details"
"roles": "Roles"
}

View File

@@ -27,7 +27,7 @@ const signozPackages = Object.keys(allDeps).filter((dep) =>
const fileContent = `// -------------------------------------------------------------------------
// AUTO-GENERATED FILE
// -------------------------------------------------------------------------
// This file is generated by scripts/update-registry.cjs automatically
// This file is generated by scripts/update-registry.js automatically
// whenever you run 'yarn install' or 'npm install'.
//
// It forces VS Code to index these specific packages to fix auto-import

View File

@@ -218,9 +218,9 @@ function App(): JSX.Element {
pathname === ROUTES.ONBOARDING ||
pathname.startsWith('/public/dashboard/')
) {
window.Pylon?.('hideChatBubble');
window.Pylon('hideChatBubble');
} else {
window.Pylon?.('showChatBubble');
window.Pylon('showChatBubble');
}
}, [pathname]);

View File

@@ -165,6 +165,11 @@ export const MySettings = Loadable(
() => import(/* webpackChunkName: "All MySettings" */ 'pages/Settings'),
);
export const CustomDomainSettings = Loadable(
() =>
import(/* webpackChunkName: "Custom Domain Settings" */ 'pages/Settings'),
);
export const Logs = Loadable(
() => import(/* webpackChunkName: "Logs" */ 'pages/LogsModulePage'),
);

View File

@@ -2,7 +2,7 @@ import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import APIError from 'types/api/error';
// @deprecated Use convertToApiError instead
// Handles errors from generated API hooks (which use RenderErrorResponseDTO)
export function ErrorResponseHandlerForGeneratedAPIs(
error: AxiosError<RenderErrorResponseDTO>,
): never {
@@ -46,34 +46,3 @@ export function ErrorResponseHandlerForGeneratedAPIs(
},
});
}
// convertToApiError converts an AxiosError from generated API
// hooks into an APIError.
export function convertToApiError(
error: AxiosError<RenderErrorResponseDTO> | null,
): APIError | undefined {
if (!error) {
return undefined;
}
const response = error.response;
const errorData = response?.data?.error;
return new APIError({
httpStatusCode: response?.status || error.status || 500,
error: {
code:
errorData?.code ||
String(response?.status || error.code || 'unknown_error'),
message:
errorData?.message ||
response?.statusText ||
error.message ||
'Something went wrong',
url: errorData?.url ?? '',
errors: (errorData?.errors ?? []).map((e) => ({
message: e.message ?? '',
})),
},
});
}

View File

@@ -29,6 +29,10 @@ import type {
UpdateAuthDomainPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists all auth domains
* @summary List all auth domains

View File

@@ -26,6 +26,10 @@ import type {
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* Checks if the authenticated user has permissions for given transactions
* @summary Check permissions

View File

@@ -35,6 +35,10 @@ import type {
UpdatePublicDashboardPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint deletes the public sharing config and disables the public sharing of a dashboard
* @summary Delete public dashboard

View File

@@ -18,6 +18,10 @@ import type { ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { GetFeatures200, RenderErrorResponseDTO } from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns the supported features and their details
* @summary Get features

View File

@@ -24,6 +24,10 @@ import type {
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns field keys
* @summary Get field keys

View File

@@ -37,6 +37,10 @@ import type {
UpdateIngestionKeyPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns the ingestion keys for a workspace
* @summary Get ingestion keys for workspace

View File

@@ -21,6 +21,10 @@ import type {
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns global config
* @summary Get global config

View File

@@ -25,6 +25,10 @@ import type {
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoints promotes and indexes paths
* @summary Promote and index paths

View File

@@ -42,6 +42,10 @@ import type {
UpdateMetricMetadataPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns a list of distinct metric names within the specified time range
* @summary List metric names

View File

@@ -25,6 +25,10 @@ import type {
TypesOrganizationDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint returns the organization I belong to
* @summary Get my organization

View File

@@ -32,6 +32,10 @@ import type {
UpdateUserPreferencePathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists all org preferences
* @summary List org preferences

View File

@@ -20,6 +20,10 @@ import type {
ReplaceVariables200,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* Execute a composite query over a time range. Supports builder queries (traces, logs, metrics), formulas, trace operators, PromQL, and ClickHouse SQL.
* @summary Query range

View File

@@ -35,6 +35,10 @@ import type {
RoletypesPostableRoleDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists all roles
* @summary List roles

View File

@@ -1,744 +0,0 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
QueryClient,
QueryFunction,
QueryKey,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useQuery } from 'react-query';
import type { ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
GetRuleHistoryFilterKeys200,
GetRuleHistoryFilterKeysParams,
GetRuleHistoryFilterKeysPathParameters,
GetRuleHistoryFilterValues200,
GetRuleHistoryFilterValuesParams,
GetRuleHistoryFilterValuesPathParameters,
GetRuleHistoryOverallStatus200,
GetRuleHistoryOverallStatusParams,
GetRuleHistoryOverallStatusPathParameters,
GetRuleHistoryStats200,
GetRuleHistoryStatsParams,
GetRuleHistoryStatsPathParameters,
GetRuleHistoryTimeline200,
GetRuleHistoryTimelineParams,
GetRuleHistoryTimelinePathParameters,
GetRuleHistoryTopContributors200,
GetRuleHistoryTopContributorsParams,
GetRuleHistoryTopContributorsPathParameters,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
/**
* Returns distinct label keys from rule history entries for the selected range.
* @summary Get rule history filter keys
*/
export const getRuleHistoryFilterKeys = (
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryFilterKeys200>({
url: `/api/v2/rules/${id}/history/filter_keys`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryFilterKeysQueryKey = (
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
) => {
return [
`/api/v2/rules/${id}/history/filter_keys`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryFilterKeysQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetRuleHistoryFilterKeysQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>
> = ({ signal }) => getRuleHistoryFilterKeys({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryFilterKeysQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>
>;
export type GetRuleHistoryFilterKeysQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule history filter keys
*/
export function useGetRuleHistoryFilterKeys<
TData = Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryFilterKeysQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule history filter keys
*/
export const invalidateGetRuleHistoryFilterKeys = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryFilterKeysPathParameters,
params?: GetRuleHistoryFilterKeysParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryFilterKeysQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns distinct label values for a given key from rule history entries.
* @summary Get rule history filter values
*/
export const getRuleHistoryFilterValues = (
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryFilterValues200>({
url: `/api/v2/rules/${id}/history/filter_values`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryFilterValuesQueryKey = (
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
) => {
return [
`/api/v2/rules/${id}/history/filter_values`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryFilterValuesQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetRuleHistoryFilterValuesQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>
> = ({ signal }) => getRuleHistoryFilterValues({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryFilterValuesQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>
>;
export type GetRuleHistoryFilterValuesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule history filter values
*/
export function useGetRuleHistoryFilterValues<
TData = Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryFilterValuesQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule history filter values
*/
export const invalidateGetRuleHistoryFilterValues = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryFilterValuesPathParameters,
params?: GetRuleHistoryFilterValuesParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryFilterValuesQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns overall firing/inactive intervals for a rule in the selected time range.
* @summary Get rule overall status timeline
*/
export const getRuleHistoryOverallStatus = (
{ id }: GetRuleHistoryOverallStatusPathParameters,
params: GetRuleHistoryOverallStatusParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryOverallStatus200>({
url: `/api/v2/rules/${id}/history/overall_status`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryOverallStatusQueryKey = (
{ id }: GetRuleHistoryOverallStatusPathParameters,
params?: GetRuleHistoryOverallStatusParams,
) => {
return [
`/api/v2/rules/${id}/history/overall_status`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryOverallStatusQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryOverallStatusPathParameters,
params: GetRuleHistoryOverallStatusParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetRuleHistoryOverallStatusQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>
> = ({ signal }) => getRuleHistoryOverallStatus({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryOverallStatusQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>
>;
export type GetRuleHistoryOverallStatusQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule overall status timeline
*/
export function useGetRuleHistoryOverallStatus<
TData = Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryOverallStatusPathParameters,
params: GetRuleHistoryOverallStatusParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryOverallStatusQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule overall status timeline
*/
export const invalidateGetRuleHistoryOverallStatus = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryOverallStatusPathParameters,
params: GetRuleHistoryOverallStatusParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryOverallStatusQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns trigger and resolution statistics for a rule in the selected time range.
* @summary Get rule history stats
*/
export const getRuleHistoryStats = (
{ id }: GetRuleHistoryStatsPathParameters,
params: GetRuleHistoryStatsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryStats200>({
url: `/api/v2/rules/${id}/history/stats`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryStatsQueryKey = (
{ id }: GetRuleHistoryStatsPathParameters,
params?: GetRuleHistoryStatsParams,
) => {
return [
`/api/v2/rules/${id}/history/stats`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryStatsQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryStatsPathParameters,
params: GetRuleHistoryStatsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetRuleHistoryStatsQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryStats>>
> = ({ signal }) => getRuleHistoryStats({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryStatsQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryStats>>
>;
export type GetRuleHistoryStatsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule history stats
*/
export function useGetRuleHistoryStats<
TData = Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryStatsPathParameters,
params: GetRuleHistoryStatsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryStats>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryStatsQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule history stats
*/
export const invalidateGetRuleHistoryStats = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryStatsPathParameters,
params: GetRuleHistoryStatsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryStatsQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns paginated timeline entries for rule state transitions.
* @summary Get rule history timeline
*/
export const getRuleHistoryTimeline = (
{ id }: GetRuleHistoryTimelinePathParameters,
params: GetRuleHistoryTimelineParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryTimeline200>({
url: `/api/v2/rules/${id}/history/timeline`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryTimelineQueryKey = (
{ id }: GetRuleHistoryTimelinePathParameters,
params?: GetRuleHistoryTimelineParams,
) => {
return [
`/api/v2/rules/${id}/history/timeline`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryTimelineQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryTimelinePathParameters,
params: GetRuleHistoryTimelineParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetRuleHistoryTimelineQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>
> = ({ signal }) => getRuleHistoryTimeline({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryTimelineQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>
>;
export type GetRuleHistoryTimelineQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get rule history timeline
*/
export function useGetRuleHistoryTimeline<
TData = Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryTimelinePathParameters,
params: GetRuleHistoryTimelineParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryTimelineQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get rule history timeline
*/
export const invalidateGetRuleHistoryTimeline = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryTimelinePathParameters,
params: GetRuleHistoryTimelineParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryTimelineQueryKey({ id }, params) },
options,
);
return queryClient;
};
/**
* Returns top label combinations contributing to rule firing in the selected time range.
* @summary Get top contributors to rule firing
*/
export const getRuleHistoryTopContributors = (
{ id }: GetRuleHistoryTopContributorsPathParameters,
params: GetRuleHistoryTopContributorsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleHistoryTopContributors200>({
url: `/api/v2/rules/${id}/history/top_contributors`,
method: 'GET',
params,
signal,
});
};
export const getGetRuleHistoryTopContributorsQueryKey = (
{ id }: GetRuleHistoryTopContributorsPathParameters,
params?: GetRuleHistoryTopContributorsParams,
) => {
return [
`/api/v2/rules/${id}/history/top_contributors`,
...(params ? [params] : []),
] as const;
};
export const getGetRuleHistoryTopContributorsQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryTopContributorsPathParameters,
params: GetRuleHistoryTopContributorsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetRuleHistoryTopContributorsQueryKey({ id }, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>
> = ({ signal }) => getRuleHistoryTopContributors({ id }, params, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleHistoryTopContributorsQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>
>;
export type GetRuleHistoryTopContributorsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get top contributors to rule firing
*/
export function useGetRuleHistoryTopContributors<
TData = Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleHistoryTopContributorsPathParameters,
params: GetRuleHistoryTopContributorsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleHistoryTopContributorsQueryOptions(
{ id },
params,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get top contributors to rule firing
*/
export const invalidateGetRuleHistoryTopContributors = async (
queryClient: QueryClient,
{ id }: GetRuleHistoryTopContributorsPathParameters,
params: GetRuleHistoryTopContributorsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleHistoryTopContributorsQueryKey({ id }, params) },
options,
);
return queryClient;
};

View File

@@ -41,6 +41,10 @@ import type {
UpdateServiceAccountStatusPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint lists the service accounts for an organisation
* @summary List service accounts

View File

@@ -33,6 +33,10 @@ import type {
RotateSession200,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint creates a session for a user using google callback
* @summary Create session by google callback

View File

@@ -2090,139 +2090,6 @@ export interface RoletypesRoleDTO {
updatedAt?: Date;
}
export enum RulestatehistorytypesAlertStateDTO {
inactive = 'inactive',
pending = 'pending',
recovering = 'recovering',
firing = 'firing',
nodata = 'nodata',
disabled = 'disabled',
}
export interface RulestatehistorytypesRuleStateHistoryContributorResponseDTO {
/**
* @type integer
* @minimum 0
*/
count: number;
/**
* @type integer
* @minimum 0
*/
fingerprint: number;
/**
* @type array
* @nullable true
*/
labels: Querybuildertypesv5LabelDTO[] | null;
/**
* @type string
*/
relatedLogsLink?: string;
/**
* @type string
*/
relatedTracesLink?: string;
}
export interface RulestatehistorytypesRuleStateHistoryResponseItemDTO {
/**
* @type integer
* @minimum 0
*/
fingerprint: number;
/**
* @type array
* @nullable true
*/
labels: Querybuildertypesv5LabelDTO[] | null;
overallState: RulestatehistorytypesAlertStateDTO;
/**
* @type boolean
*/
overallStateChanged: boolean;
/**
* @type string
*/
ruleID: string;
/**
* @type string
*/
ruleName: string;
state: RulestatehistorytypesAlertStateDTO;
/**
* @type boolean
*/
stateChanged: boolean;
/**
* @type integer
* @format int64
*/
unixMilli: number;
/**
* @type number
* @format double
*/
value: number;
}
export interface RulestatehistorytypesRuleStateTimelineResponseDTO {
/**
* @type array
* @nullable true
*/
items: RulestatehistorytypesRuleStateHistoryResponseItemDTO[] | null;
/**
* @type string
*/
nextCursor?: string;
/**
* @type integer
* @minimum 0
*/
total: number;
}
export interface RulestatehistorytypesRuleStateWindowDTO {
/**
* @type integer
* @format int64
*/
end: number;
/**
* @type integer
* @format int64
*/
start: number;
state: RulestatehistorytypesAlertStateDTO;
}
export interface RulestatehistorytypesStatsDTO {
/**
* @type number
* @format double
*/
currentAvgResolutionTime: number;
currentAvgResolutionTimeSeries: Querybuildertypesv5TimeSeriesDTO;
currentTriggersSeries: Querybuildertypesv5TimeSeriesDTO;
/**
* @type number
* @format double
*/
pastAvgResolutionTime: number;
pastAvgResolutionTimeSeries: Querybuildertypesv5TimeSeriesDTO;
pastTriggersSeries: Querybuildertypesv5TimeSeriesDTO;
/**
* @type integer
* @minimum 0
*/
totalCurrentTriggers: number;
/**
* @type integer
* @minimum 0
*/
totalPastTriggers: number;
}
export interface ServiceaccounttypesFactorAPIKeyDTO {
/**
* @type string
@@ -3584,11 +3451,6 @@ export type ListMetricsParams = {
* @description undefined
*/
searchText?: string;
/**
* @type string
* @description undefined
*/
source?: string;
};
export type ListMetrics200 = {
@@ -3696,266 +3558,6 @@ export type GetMyOrganization200 = {
status: string;
};
export type GetRuleHistoryFilterKeysPathParameters = {
id: string;
};
export type GetRuleHistoryFilterKeysParams = {
/**
* @description undefined
*/
signal?: TelemetrytypesSignalDTO;
/**
* @description undefined
*/
source?: TelemetrytypesSourceDTO;
/**
* @type integer
* @description undefined
*/
limit?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
startUnixMilli?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
endUnixMilli?: number;
/**
* @description undefined
*/
fieldContext?: TelemetrytypesFieldContextDTO;
/**
* @description undefined
*/
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
/**
* @type string
* @description undefined
*/
metricName?: string;
/**
* @type string
* @description undefined
*/
searchText?: string;
};
export type GetRuleHistoryFilterKeys200 = {
data: TelemetrytypesGettableFieldKeysDTO;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryFilterValuesPathParameters = {
id: string;
};
export type GetRuleHistoryFilterValuesParams = {
/**
* @description undefined
*/
signal?: TelemetrytypesSignalDTO;
/**
* @description undefined
*/
source?: TelemetrytypesSourceDTO;
/**
* @type integer
* @description undefined
*/
limit?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
startUnixMilli?: number;
/**
* @type integer
* @format int64
* @description undefined
*/
endUnixMilli?: number;
/**
* @description undefined
*/
fieldContext?: TelemetrytypesFieldContextDTO;
/**
* @description undefined
*/
fieldDataType?: TelemetrytypesFieldDataTypeDTO;
/**
* @type string
* @description undefined
*/
metricName?: string;
/**
* @type string
* @description undefined
*/
searchText?: string;
/**
* @type string
* @description undefined
*/
name?: string;
/**
* @type string
* @description undefined
*/
existingQuery?: string;
};
export type GetRuleHistoryFilterValues200 = {
data: TelemetrytypesGettableFieldValuesDTO;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryOverallStatusPathParameters = {
id: string;
};
export type GetRuleHistoryOverallStatusParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
start: number;
/**
* @type integer
* @format int64
* @description undefined
*/
end: number;
};
export type GetRuleHistoryOverallStatus200 = {
/**
* @type array
* @nullable true
*/
data: RulestatehistorytypesRuleStateWindowDTO[] | null;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryStatsPathParameters = {
id: string;
};
export type GetRuleHistoryStatsParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
start: number;
/**
* @type integer
* @format int64
* @description undefined
*/
end: number;
};
export type GetRuleHistoryStats200 = {
data: RulestatehistorytypesStatsDTO;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryTimelinePathParameters = {
id: string;
};
export type GetRuleHistoryTimelineParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
start: number;
/**
* @type integer
* @format int64
* @description undefined
*/
end: number;
/**
* @description undefined
*/
state?: RulestatehistorytypesAlertStateDTO;
/**
* @type string
* @description undefined
*/
filterExpression?: string;
/**
* @type integer
* @format int64
* @description undefined
*/
limit?: number;
/**
* @description undefined
*/
order?: Querybuildertypesv5OrderDirectionDTO;
/**
* @type string
* @description undefined
*/
cursor?: string;
};
export type GetRuleHistoryTimeline200 = {
data: RulestatehistorytypesRuleStateTimelineResponseDTO;
/**
* @type string
*/
status: string;
};
export type GetRuleHistoryTopContributorsPathParameters = {
id: string;
};
export type GetRuleHistoryTopContributorsParams = {
/**
* @type integer
* @format int64
* @description undefined
*/
start: number;
/**
* @type integer
* @format int64
* @description undefined
*/
end: number;
};
export type GetRuleHistoryTopContributors200 = {
/**
* @type array
* @nullable true
*/
data: RulestatehistorytypesRuleStateHistoryContributorResponseDTO[] | null;
/**
* @type string
*/
status: string;
};
export type GetSessionContext200 = {
data: AuthtypesSessionContextDTO;
/**

View File

@@ -51,6 +51,10 @@ import type {
UpdateUserPathParameters,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint changes the password by id
* @summary Change password

View File

@@ -26,6 +26,10 @@ import type {
ZeustypesPostableProfileDTO,
} from '../sigNoz.schemas';
type AwaitedInput<T> = PromiseLike<T> | T;
type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;
/**
* This endpoint gets the host info from zeus.
* @summary Get host info from Zeus.

View File

@@ -0,0 +1,54 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { TreemapViewType } from 'container/MetricsExplorer/Summary/types';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface MetricsTreeMapPayload {
filters: TagFilter;
limit?: number;
treemap?: TreemapViewType;
}
export interface MetricsTreeMapResponse {
status: string;
data: {
[TreemapViewType.TIMESERIES]: TimeseriesData[];
[TreemapViewType.SAMPLES]: SamplesData[];
};
}
export interface TimeseriesData {
percentage: number;
total_value: number;
metric_name: string;
}
export interface SamplesData {
percentage: number;
metric_name: string;
}
export const getMetricsTreeMap = async (
props: MetricsTreeMapPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<MetricsTreeMapResponse> | ErrorResponse> => {
try {
const response = await axios.post('/metrics/treemap', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -0,0 +1,36 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Temporality } from './getMetricDetails';
import { MetricType } from './getMetricsList';
export interface UpdateMetricMetadataProps {
description: string;
metricType: MetricType;
temporality?: Temporality;
isMonotonic?: boolean;
unit?: string;
}
export interface UpdateMetricMetadataResponse {
success: boolean;
message: string;
}
const updateMetricMetadata = async (
metricName: string,
props: UpdateMetricMetadataProps,
): Promise<SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse> => {
const response = await axios.post(`/metrics/${metricName}/metadata`, {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateMetricMetadata;

View File

@@ -1,6 +1,5 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import store from 'store';
import {
QueryKeyRequestProps,
QueryKeySuggestionsResponseProps,
@@ -18,12 +17,6 @@ export const getKeySuggestions = (
signalSource = '',
} = props;
const { globalTime } = store.getState();
const resolvedTimeRange = {
startUnixMilli: Math.floor(globalTime.minTime / 1000000),
endUnixMilli: Math.floor(globalTime.maxTime / 1000000),
};
const encodedSignal = encodeURIComponent(signal);
const encodedSearchText = encodeURIComponent(searchText);
const encodedMetricName = encodeURIComponent(metricName);
@@ -31,14 +24,7 @@ export const getKeySuggestions = (
const encodedFieldDataType = encodeURIComponent(fieldDataType);
const encodedSource = encodeURIComponent(signalSource);
let url = `/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`;
if (resolvedTimeRange.startUnixMilli !== undefined) {
url += `&startUnixMilli=${resolvedTimeRange.startUnixMilli}`;
}
if (resolvedTimeRange.endUnixMilli !== undefined) {
url += `&endUnixMilli=${resolvedTimeRange.endUnixMilli}`;
}
return axios.get(url);
return axios.get(
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`,
);
};

View File

@@ -1,6 +1,5 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import store from 'store';
import {
QueryKeyValueRequestProps,
QueryKeyValueSuggestionsResponseProps,
@@ -9,20 +8,7 @@ import {
export const getValueSuggestions = (
props: QueryKeyValueRequestProps,
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
const {
signal,
key,
searchText,
signalSource,
metricName,
existingQuery,
} = props;
const { globalTime } = store.getState();
const resolvedTimeRange = {
startUnixMilli: Math.floor(globalTime.minTime / 1000000),
endUnixMilli: Math.floor(globalTime.maxTime / 1000000),
};
const { signal, key, searchText, signalSource, metricName } = props;
const encodedSignal = encodeURIComponent(signal);
const encodedKey = encodeURIComponent(key);
@@ -30,17 +16,7 @@ export const getValueSuggestions = (
const encodedSearchText = encodeURIComponent(searchText);
const encodedSource = encodeURIComponent(signalSource || '');
let url = `/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&source=${encodedSource}`;
if (resolvedTimeRange.startUnixMilli !== undefined) {
url += `&startUnixMilli=${resolvedTimeRange.startUnixMilli}`;
}
if (resolvedTimeRange.endUnixMilli !== undefined) {
url += `&endUnixMilli=${resolvedTimeRange.endUnixMilli}`;
}
if (existingQuery) {
url += `&existingQuery=${encodeURIComponent(existingQuery)}`;
}
return axios.get(url);
return axios.get(
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&source=${encodedSource}`,
);
};

View File

@@ -1,7 +1,7 @@
// -------------------------------------------------------------------------
// AUTO-GENERATED FILE
// -------------------------------------------------------------------------
// This file is generated by scripts/update-registry.cjs automatically
// This file is generated by scripts/update-registry.js automatically
// whenever you run 'yarn install' or 'npm install'.
//
// It forces VS Code to index these specific packages to fix auto-import
@@ -18,7 +18,6 @@ import '@signozhq/checkbox';
import '@signozhq/combobox';
import '@signozhq/command';
import '@signozhq/design-tokens';
import '@signozhq/dialog';
import '@signozhq/icons';
import '@signozhq/input';
import '@signozhq/popover';
@@ -27,5 +26,4 @@ import '@signozhq/resizable';
import '@signozhq/sonner';
import '@signozhq/switch';
import '@signozhq/table';
import '@signozhq/toggle-group';
import '@signozhq/tooltip';

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
<DocumentFragment>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Editor renders correctly with custom props 1`] = `
<div>

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MessageTip custom action 1`] = `
.c0 {

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Not Found page test should render Not Found page without errors 1`] = `
<DocumentFragment>

View File

@@ -60,30 +60,11 @@
gap: 8px;
margin-left: 108px;
position: relative;
/* Vertical dashed line connecting query elements */
&::after {
content: '';
position: absolute;
left: -28px;
top: 0;
bottom: 0;
width: 1px;
background: repeating-linear-gradient(
to bottom,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
}
.code-mirror-where-clause,
.query-aggregation-container,
.query-add-ons,
.metrics-aggregation-section-content,
.metrics-container {
.metrics-aggregation-section-content {
position: relative;
&::before {
@@ -121,10 +102,6 @@
.qb-elements-container {
margin-left: 0px;
&::after {
display: none;
}
.code-mirror-where-clause,
.query-aggregation-container,
.query-add-ons,
@@ -356,7 +333,28 @@
text-transform: uppercase;
&::before {
display: none;
content: '';
height: 120px;
content: '';
position: absolute;
left: 0;
top: 31px;
bottom: 0;
width: 1px;
background: repeating-linear-gradient(
to bottom,
#1d212d,
#1d212d 4px,
transparent 4px,
transparent 8px
);
left: 15px;
}
&.has-trace-operator {
&::before {
height: 0px;
}
}
}
@@ -464,21 +462,10 @@
.qb-content-section {
.qb-elements-container {
&::after {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
.code-mirror-where-clause,
.query-aggregation-container,
.query-add-ons,
.metrics-aggregation-section-content,
.metrics-container {
.metrics-aggregation-section-content {
&::before {
border-left: 6px dotted var(--bg-vanilla-300);
}
@@ -542,6 +529,18 @@
.qb-entity-options {
.options {
.query-name {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
.formula-name {
&::before {
background: repeating-linear-gradient(

View File

@@ -207,7 +207,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
signalSource={currentQuery.builder.queryData[0].source as 'meter' | ''}
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}
queriesCount={1}

View File

@@ -1,13 +1,14 @@
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { Select } from 'antd';
import {
initialQueriesMap,
initialQueryMeterWithType,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { MetricNameSelector } from 'container/QueryBuilder/filters';
import { AggregatorFilter } from 'container/QueryBuilder/filters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
@@ -43,12 +44,21 @@ export const MetricsSelect = memo(function MetricsSelect({
signalSourceChangeEnabled: boolean;
savePreviousQuery: boolean;
}): JSX.Element {
const [attributeKeys, setAttributeKeys] = useState<BaseAutocompleteData[]>([]);
const { handleChangeAggregatorAttribute } = useQueryOperations({
index,
query,
entityVersion: version,
});
const handleAggregatorAttributeChange = useCallback(
(value: BaseAutocompleteData, isEditMode?: boolean) => {
handleChangeAggregatorAttribute(value, isEditMode, attributeKeys || []);
},
[handleChangeAggregatorAttribute, attributeKeys],
);
const {
updateAllQueriesOperators,
handleSetQueryData,
@@ -154,10 +164,12 @@ export const MetricsSelect = memo(function MetricsSelect({
/>
)}
<MetricNameSelector
onChange={handleChangeAggregatorAttribute}
<AggregatorFilter
onChange={handleAggregatorAttributeChange}
query={query}
index={index}
signalSource={signalSource || ''}
setAttributeKeys={setAttributeKeys}
/>
</div>
);

View File

@@ -202,8 +202,8 @@ function QueryAddOns({
} else {
filteredAddOns = Object.values(ADD_ONS);
// Filter out group_by for metrics data source
if (query.dataSource === DataSource.METRICS) {
// Filter out group_by for metrics data source (handled in MetricsAggregateSection)
filteredAddOns = filteredAddOns.filter(
(addOn) => addOn.key !== ADD_ONS_KEYS.GROUP_BY,
);

View File

@@ -272,6 +272,7 @@ function QuerySearch({
metricName: debouncedMetricName ?? undefined,
signalSource: signalSource as 'meter' | '',
});
if (response.data.data) {
const { keys } = response.data.data;
const options = generateOptions(keys);
@@ -431,7 +432,6 @@ function QuerySearch({
}
const sanitizedSearchText = searchText ? searchText?.trim() : '';
const existingQuery = queryData.filter?.expression || '';
try {
const response = await getValueSuggestions({
@@ -440,9 +440,9 @@ function QuerySearch({
signal: dataSource,
signalSource: signalSource as 'meter' | '',
metricName: debouncedMetricName ?? undefined,
existingQuery,
}); // Skip updates if component unmounted or key changed
});
// Skip updates if component unmounted or key changed
if (
!isMountedRef.current ||
lastKeyRef.current !== key ||
@@ -454,9 +454,7 @@ function QuerySearch({
// Process the response data
const responseData = response.data as any;
const values = responseData.data?.values || {};
const relatedValues = values.relatedValues || [];
const stringValues =
relatedValues.length > 0 ? relatedValues : values.stringValues || [];
const stringValues = values.stringValues || [];
const numberValues = values.numberValues || [];
// Generate options from string values - explicitly handle empty strings
@@ -531,12 +529,11 @@ function QuerySearch({
},
[
activeKey,
isLoadingSuggestions,
queryData.filter?.expression,
toggleSuggestions,
dataSource,
signalSource,
isLoadingSuggestions,
debouncedMetricName,
signalSource,
toggleSuggestions,
],
);
@@ -1243,17 +1240,19 @@ function QuerySearch({
if (!queryContext) {
return;
}
// Only trigger suggestions and fetch if editor is focused (i.e., user is interacting)
if (isFocused && editorRef.current) {
// Trigger suggestions based on context
if (editorRef.current) {
toggleSuggestions(10);
// Handle value suggestions for value context
if (queryContext.isInValue) {
const { keyToken, currentToken } = queryContext;
const key = keyToken || currentToken;
// Only fetch if needed and if we have a valid key
if (key && key !== activeKey && !isLoadingSuggestions) {
fetchValueSuggestions({ key });
}
}
// Handle value suggestions for value context
if (queryContext.isInValue) {
const { keyToken, currentToken } = queryContext;
const key = keyToken || currentToken;
// Only fetch if needed and if we have a valid key
if (key && key !== activeKey && !isLoadingSuggestions) {
fetchValueSuggestions({ key });
}
}
}, [
@@ -1262,7 +1261,6 @@ function QuerySearch({
isLoadingSuggestions,
activeKey,
fetchValueSuggestions,
isFocused,
]);
const getTooltipContent = (): JSX.Element => (

View File

@@ -43,7 +43,6 @@ jest.mock(
);
jest.mock('container/QueryBuilder/filters', () => ({
AggregatorFilter: (): JSX.Element => <div />,
MetricNameSelector: (): JSX.Element => <div />,
}));
// Mock hooks
jest.mock('hooks/queryBuilder/useQueryBuilder');

View File

@@ -48,12 +48,7 @@
.filter-separator {
height: 1px;
background-color: var(--bg-slate-400);
margin: 7px 0;
&.related-separator {
opacity: 0.5;
margin: 0.5px 0;
}
margin: 4px 0;
}
.value {
@@ -143,93 +138,6 @@
cursor: pointer;
}
}
.search-prompt {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 12px;
margin-top: 4px;
border: 1px dashed var(--bg-amber-500);
border-radius: 10px;
color: var(--bg-amber-200);
background: linear-gradient(
90deg,
var(--bg-ink-500) 0%,
var(--bg-ink-400) 100%
);
cursor: pointer;
transition: all 0.16s ease, transform 0.12s ease;
text-align: left;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
&:hover {
background: linear-gradient(
90deg,
var(--bg-ink-400) 0%,
var(--bg-ink-300) 100%
);
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.45);
}
&:active {
transform: translateY(1px);
}
&__icon {
color: var(--bg-amber-400);
flex-shrink: 0;
}
&__text {
display: flex;
flex-direction: column;
gap: 2px;
}
&__title {
color: var(--bg-amber-200);
}
&__subtitle {
color: var(--bg-amber-300);
font-size: 12px;
}
}
.lightMode & {
.search-prompt {
border: 1px dashed var(--bg-amber-500);
color: var(--bg-amber-800);
background: linear-gradient(
90deg,
var(--bg-vanilla-200) 0%,
var(--bg-vanilla-100) 100%
);
box-shadow: 0 2px 12px rgba(184, 107, 0, 0.08);
&:hover {
background: linear-gradient(
90deg,
var(--bg-vanilla-100) 0%,
var(--bg-vanilla-50) 100%
);
box-shadow: 0 4px 16px rgba(184, 107, 0, 0.15);
}
&__icon {
color: var(--bg-amber-600);
}
&__title {
color: var(--bg-amber-800);
}
&__subtitle {
color: var(--bg-amber-800);
}
}
}
.go-to-docs {
display: flex;
flex-direction: column;

View File

@@ -150,8 +150,7 @@ describe('CheckboxFilter - User Flows', () => {
// User should see the filter is automatically opened (not collapsed)
expect(screen.getByText('Service Name')).toBeInTheDocument();
await waitFor(() => {
// eslint-disable-next-line sonarjs/no-duplicate-string
expect(screen.getByPlaceholderText('Search values')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
});
// User should see visual separator between checked and unchecked items
@@ -185,7 +184,7 @@ describe('CheckboxFilter - User Flows', () => {
// Initially auto-opened due to active filters
await waitFor(() => {
expect(screen.getByPlaceholderText('Search values')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
});
// User manually closes the filter
@@ -193,7 +192,7 @@ describe('CheckboxFilter - User Flows', () => {
// User should see filter is now closed (respecting user preference)
expect(
screen.queryByPlaceholderText('Search values'),
screen.queryByPlaceholderText('Filter values'),
).not.toBeInTheDocument();
// User manually opens the filter again
@@ -201,7 +200,7 @@ describe('CheckboxFilter - User Flows', () => {
// User should see filter is now open (respecting user preference)
await waitFor(() => {
expect(screen.getByPlaceholderText('Search values')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
});
});

View File

@@ -1,15 +1,6 @@
/* eslint-disable sonarjs/no-identical-functions */
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import {
Fragment,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Button, Checkbox, Input, InputRef, Skeleton, Typography } from 'antd';
import { Fragment, useMemo, useState } from 'react';
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
import {
@@ -17,14 +8,19 @@ import {
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { OPERATORS } from 'constants/antlrQueryConstants';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import { AlertTriangle, ChevronDown, ChevronRight } from 'lucide-react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
@@ -61,7 +57,6 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
// null = no user action, true = user opened, false = user closed
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
const [visibleUncheckedCount, setVisibleUncheckedCount] = useState<number>(5);
const {
lastUsedQuery,
@@ -83,12 +78,6 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
return lastUsedQuery || 0;
}, [isListView, source, lastUsedQuery]);
// Extract current filter expression for the active query
const currentFilterExpression = useMemo(() => {
const queryData = currentQuery.builder.queryData?.[activeQueryIndex];
return queryData?.filter?.expression || '';
}, [currentQuery.builder.queryData, activeQueryIndex]);
// Check if this filter has active filters in the query
const isSomeFilterPresentForCurrentAttribute = useMemo(
() =>
@@ -120,125 +109,54 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
filter.defaultOpen,
]);
const { data, isLoading } = useGetAggregateValues(
{
aggregateOperator: filter.aggregateOperator || 'noop',
dataSource: filter.dataSource || DataSource.LOGS,
aggregateAttribute: filter.aggregateAttribute || '',
attributeKey: filter.attributeKey.key,
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
tagType: filter.attributeKey.type || '',
searchText: searchText ?? '',
},
{
enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER,
keepPreviousData: true,
},
);
const {
data: keyValueSuggestions,
isLoading: isLoadingKeyValueSuggestions,
refetch: refetchKeyValueSuggestions,
} = useGetQueryKeyValueSuggestions({
key: filter.attributeKey.key,
signal: filter.dataSource || DataSource.LOGS,
signalSource: 'meter',
searchText: searchText || '',
existingQuery: currentFilterExpression,
options: {
enabled: isOpen,
enabled: isOpen && source === QuickFiltersSource.METER_EXPLORER,
keepPreviousData: true,
},
});
const searchInputRef = useRef<InputRef | null>(null);
const searchContainerRef = useRef<HTMLDivElement | null>(null);
const previousFiltersItemsRef = useRef(
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items,
);
const attributeValues: string[] = useMemo(() => {
const dataType = filter.attributeKey.dataType || DataTypes.String;
// Refetch when other filters change (not this filter)
// Watch for when filters.items is different from previous value, indicating other filters changed
useEffect(() => {
const currentFiltersItems =
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items;
const previousFiltersItems = previousFiltersItemsRef.current;
// Check if filters items have changed (not the same)
const filtersChanged = !isEqual(previousFiltersItems, currentFiltersItems);
if (isOpen && filtersChanged) {
// Check if OTHER filters (not this filter) have changed
const currentOtherFilters = currentFiltersItems?.filter(
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
const previousOtherFilters = previousFiltersItems?.filter(
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
);
// Refetch if other filters changed (not just this filter's values)
const otherFiltersChanged = !isEqual(
currentOtherFilters,
previousOtherFilters,
);
// Only update ref if we have valid API data or if filters actually changed
// Don't update if search returned 0 results to preserve unchecked values
const hasValidData = keyValueSuggestions && !isLoadingKeyValueSuggestions;
if (otherFiltersChanged || hasValidData) {
previousFiltersItemsRef.current = currentFiltersItems;
}
if (otherFiltersChanged) {
refetchKeyValueSuggestions();
}
} else {
previousFiltersItemsRef.current = currentFiltersItems;
}
}, [
activeQueryIndex,
isOpen,
refetchKeyValueSuggestions,
filter.attributeKey.key,
currentQuery.builder.queryData,
keyValueSuggestions,
isLoadingKeyValueSuggestions,
]);
const handleSearchPromptClick = useCallback((): void => {
if (searchContainerRef.current) {
searchContainerRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
if (searchInputRef.current) {
setTimeout(() => searchInputRef.current?.focus({ cursor: 'end' }), 120);
}
}, []);
const isDataComplete = useMemo(() => {
if (keyValueSuggestions) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const responseData = keyValueSuggestions?.data as any;
return responseData.data?.complete || false;
}
return false;
}, [keyValueSuggestions]);
const previousUncheckedValuesRef = useRef<string[]>([]);
const { attributeValues, relatedValuesSet } = useMemo(() => {
if (keyValueSuggestions) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (source === QuickFiltersSource.METER_EXPLORER && keyValueSuggestions) {
// Process the response data
const responseData = keyValueSuggestions?.data as any;
const values = responseData.data?.values || {};
const relatedValues: string[] = values.relatedValues || [];
const stringValues: string[] = values.stringValues || [];
const numberValues: number[] = values.numberValues || [];
const stringValues = values.stringValues || [];
const numberValues = values.numberValues || [];
const valuesToUse = [
...relatedValues,
...stringValues.filter(
(value: string | null | undefined) =>
value !== null &&
value !== undefined &&
value !== '' &&
!relatedValues.includes(value),
),
];
const stringOptions = valuesToUse.filter(
(value: string | null | undefined): value is string =>
value !== null && value !== undefined && value !== '',
);
// Generate options from string values - explicitly handle empty strings
const stringOptions = stringValues
// Strict filtering for empty string - we'll handle it as a special case if needed
.filter(
(value: string | null | undefined): value is string =>
value !== null && value !== undefined && value !== '',
);
// Generate options from number values
const numberOptions = numberValues
.filter(
(value: number | null | undefined): value is number =>
@@ -246,27 +164,15 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
)
.map((value: number) => value.toString());
const filteredRelated = new Set(
relatedValues.filter(
(v): v is string => v !== null && v !== undefined && v !== '',
),
);
const baseValues = [...stringOptions, ...numberOptions];
const previousUnchecked = previousUncheckedValuesRef.current || [];
const preservedUnchecked = previousUnchecked.filter(
(value) => !baseValues.includes(value),
);
return {
attributeValues: [...baseValues, ...preservedUnchecked],
relatedValuesSet: filteredRelated,
};
// Combine all options and make sure we don't have duplicate labels
return [...stringOptions, ...numberOptions];
}
return {
attributeValues: [] as string[],
relatedValuesSet: new Set<string>(),
};
}, [keyValueSuggestions]);
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
return (data?.payload?.[key] || []).filter(
(val) => val !== undefined && val !== null,
);
}, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]);
const setSearchTextDebounced = useDebouncedFn((...args) => {
setSearchText(args[0] as string);
@@ -340,51 +246,22 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const isMultipleValuesTrueForTheKey =
Object.values(currentFilterState).filter((val) => val).length > 1;
// Sort checked items to the top; always show unchecked items beneath, regardless of pagination
const {
visibleCheckedValues,
uncheckedValues,
visibleUncheckedValues,
visibleCheckedCount,
hasMoreChecked,
hasMoreUnchecked,
checkedSeparatorIndex,
} = useMemo(() => {
// Sort checked items to the top, then unchecked items
const currentAttributeKeys = useMemo(() => {
const checkedValues = attributeValues.filter(
(val) => currentFilterState[val],
);
const unchecked = attributeValues.filter((val) => !currentFilterState[val]);
const visibleChecked = checkedValues.slice(0, visibleItemsCount);
const visibleUnchecked = unchecked.slice(0, visibleUncheckedCount);
const uncheckedValues = attributeValues.filter(
(val) => !currentFilterState[val],
);
return [...checkedValues, ...uncheckedValues].slice(0, visibleItemsCount);
}, [attributeValues, currentFilterState, visibleItemsCount]);
const findSeparatorIndex = (list: string[]): number => {
if (relatedValuesSet.size === 0) {
return -1;
}
const firstNonRelated = list.findIndex((v) => !relatedValuesSet.has(v));
return firstNonRelated > 0 ? firstNonRelated : -1;
};
return {
visibleCheckedValues: visibleChecked,
uncheckedValues: unchecked,
visibleUncheckedValues: visibleUnchecked,
visibleCheckedCount: visibleChecked.length,
hasMoreChecked: checkedValues.length > visibleChecked.length,
hasMoreUnchecked: unchecked.length > visibleUnchecked.length,
checkedSeparatorIndex: findSeparatorIndex(visibleChecked),
};
}, [
attributeValues,
currentFilterState,
visibleItemsCount,
visibleUncheckedCount,
relatedValuesSet,
]);
useEffect(() => {
previousUncheckedValuesRef.current = uncheckedValues;
}, [uncheckedValues]);
// Count of checked values in the currently visible items
const checkedValuesCount = useMemo(
() => currentAttributeKeys.filter((val) => currentFilterState[val]).length,
[currentAttributeKeys, currentFilterState],
);
const handleClearFilterAttribute = (): void => {
const preparedQuery: Query = {
@@ -425,7 +302,6 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
isOnlyOrAllClicked: boolean,
// eslint-disable-next-line sonarjs/cognitive-complexity
): void => {
setVisibleUncheckedCount(5);
const query = cloneDeep(currentQuery.builder.queryData?.[activeQueryIndex]);
// if only or all are clicked we do not need to worry about anything just override whatever we have
@@ -686,7 +562,6 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
if (isOpen) {
setUserToggleState(false);
setVisibleItemsCount(10);
setVisibleUncheckedCount(5);
} else {
setUserToggleState(true);
}
@@ -715,93 +590,35 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
)}
</section>
</section>
{isOpen && isLoadingKeyValueSuggestions && !attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && !isLoadingKeyValueSuggestions && (
{isOpen &&
(isLoading || isLoadingKeyValueSuggestions) &&
!attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && !isLoading && !isLoadingKeyValueSuggestions && (
<>
{!isEmptyStateWithDocsEnabled && (
<section className="search" ref={searchContainerRef}>
<section className="search">
<Input
placeholder="Search values"
placeholder="Filter values"
onChange={(e): void => setSearchTextDebounced(e.target.value)}
disabled={isFilterDisabled}
ref={searchInputRef}
/>
</section>
)}
{attributeValues.length > 0 ? (
<section className="values">
{visibleCheckedValues.map((value: string, index: number) => (
{currentAttributeKeys.map((value: string, index: number) => (
<Fragment key={value}>
{index === checkedSeparatorIndex && (
<div className="filter-separator related-separator" />
)}
<div className="value">
<Checkbox
onChange={(e): void => onChange(value, e.target.checked, false)}
checked={currentFilterState[value]}
disabled={isFilterDisabled}
rootClassName="check-box"
/>
{index === checkedValuesCount && checkedValuesCount > 0 && (
<div
className={cx(
'checkbox-value-section',
isFilterDisabled ? 'filter-disabled' : '',
)}
onClick={(): void => {
if (isFilterDisabled) {
return;
}
onChange(value, currentFilterState[value], true);
}}
>
<div className={`${filter.title} label-${value}`} />
{filter.customRendererForValue ? (
filter.customRendererForValue(value)
) : (
<Typography.Text
className="value-string"
ellipsis={{ tooltip: { placement: 'top' } }}
>
{String(value)}
</Typography.Text>
)}
<Button type="text" className="only-btn">
{isSomeFilterPresentForCurrentAttribute
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
? 'All'
: 'Only'
: 'Only'}
</Button>
<Button type="text" className="toggle-btn">
Toggle
</Button>
</div>
</div>
</Fragment>
))}
{hasMoreChecked && (
<section className="show-more">
<Typography.Text
className="show-more-text"
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
>
Show More...
</Typography.Text>
</section>
)}
{visibleCheckedCount > 0 && uncheckedValues.length > 0 && (
<div className="filter-separator" data-testid="filter-separator" />
)}
{visibleUncheckedValues.map((value: string) => (
<Fragment key={value}>
key="separator"
className="filter-separator"
data-testid="filter-separator"
/>
)}
<div className="value">
<Checkbox
onChange={(e): void => onChange(value, e.target.checked, false)}
@@ -853,17 +670,6 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
</div>
</Fragment>
))}
{hasMoreUnchecked && (
<section className="show-more">
<Typography.Text
className="show-more-text"
onClick={(): void => setVisibleUncheckedCount((prev) => prev + 5)}
>
Show More...
</Typography.Text>
</section>
)}
</section>
) : isEmptyStateWithDocsEnabled ? (
<LogsQuickFilterEmptyState attributeKey={filter.attributeKey.key} />
@@ -872,18 +678,16 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
<Typography.Text>No values found</Typography.Text>{' '}
</section>
)}
{visibleItemsCount >= attributeValues?.length &&
attributeValues?.length > 0 &&
!isDataComplete && (
<section className="search-prompt" onClick={handleSearchPromptClick}>
<AlertTriangle size={16} className="search-prompt__icon" />
<span className="search-prompt__text">
<Typography.Text className="search-prompt__subtitle">
Tap to search and load more suggestions.
</Typography.Text>
</span>
</section>
)}
{visibleItemsCount < attributeValues?.length && (
<section className="show-more">
<Typography.Text
className="show-more-text"
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
>
Show More...
</Typography.Text>
</section>
)}
</>
)}
</div>

View File

@@ -127,34 +127,6 @@
align-items: center;
padding: 8px;
}
.filters-info {
display: flex;
align-items: center;
padding: 6px 10px 0 10px;
color: var(--bg-vanilla-400);
gap: 6px;
flex-wrap: wrap;
.filters-info-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0;
height: auto;
color: var(--bg-vanilla-400);
&:hover {
color: var(--bg-robin-500);
}
}
.filters-info-text {
color: var(--bg-vanilla-400);
font-size: 13px;
line-height: 16px;
}
}
}
.perilin-bg {
@@ -208,30 +180,5 @@
}
}
}
.filters-info {
color: var(--bg-ink-400);
.filters-info-toggle {
color: var(--bg-ink-400);
&:hover {
color: var(--bg-ink-300);
}
}
.filters-info-text {
color: var(--bg-ink-400);
}
}
}
}
.filters-info-tooltip-title {
font-weight: var(--font-weight-bold);
margin-bottom: 4px;
}
.filters-info-tooltip-detail {
margin-top: 4px;
}

View File

@@ -23,7 +23,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { isFunction, isNull } from 'lodash-es';
import { Frown, Lightbulb, Settings2 as SettingsIcon } from 'lucide-react';
import { Frown, Settings2 as SettingsIcon } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { USER_ROLES } from 'types/roles';
@@ -291,27 +291,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
/>
</div>
)}
<section className="filters-info">
<Tooltip
title={
<div className="filters-info-tooltip">
<div className="filters-info-tooltip-title">Adaptive Filters</div>
<div>Values update automatically as you apply filters.</div>
<div className="filters-info-tooltip-detail">
The most relevant values are shown first, followed by all other
available options.
</div>
</div>
}
placement="right"
mouseEnterDelay={0.3}
>
<Typography.Text className="filters-info-toggle">
<Lightbulb size={15} />
Adaptive filters
</Typography.Text>
</Tooltip>
</section>
<section className="filters">
{filterConfig.map((filter) => {
switch (filter.type) {

View File

@@ -4,7 +4,6 @@ import {
useApiMonitoringParams,
} from 'container/ApiMonitoring/queryParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
import {
otherFiltersResponse,
quickFiltersAttributeValuesResponse,
@@ -25,8 +24,6 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
}));
jest.mock('container/ApiMonitoring/queryParams');
jest.mock('hooks/querySuggestions/useGetQueryKeyValueSuggestions');
const handleFilterVisibilityChange = jest.fn();
const redirectWithQueryBuilderData = jest.fn();
const putHandler = jest.fn();
@@ -35,15 +32,13 @@ const mockSetApiMonitoringParams = jest.fn() as jest.MockedFunction<
>;
const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
const mockUseGetQueryKeyValueSuggestions = jest.mocked(
useGetQueryKeyValueSuggestions,
);
const BASE_URL = ENVIRONMENT.baseURL;
const SIGNAL = SignalType.LOGS;
const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`;
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
const FILTER_OS_DESCRIPTION = 'os.description';
const FILTER_K8S_DEPLOYMENT_NAME = 'k8s.deployment.name';
@@ -67,7 +62,10 @@ const setupServer = (): void => {
putHandler(await req.json());
return res(ctx.status(200), ctx.json({}));
}),
rest.get('*/api/v1/fields/values*', (_req, res, ctx) =>
rest.get(quickFiltersAttributeValuesURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
),
rest.get(fieldsValuesURL, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
),
);
@@ -137,28 +135,18 @@ beforeEach(() => {
queryData: [
{
queryName: QUERY_NAME,
filters: { items: [], op: 'AND' },
filter: { expression: '' },
filters: { items: [{ key: 'test', value: 'value' }] },
},
],
},
},
lastUsedQuery: 0,
panelType: 'logs',
redirectWithQueryBuilderData,
});
mockUseApiMonitoringParams.mockReturnValue([
{ showIP: true } as ApiMonitoringParams,
mockSetApiMonitoringParams,
]);
// Mock the hook to return data with mq-kafka
mockUseGetQueryKeyValueSuggestions.mockReturnValue({
data: quickFiltersAttributeValuesResponse,
isLoading: false,
refetch: jest.fn(),
} as any);
setupServer();
});
@@ -271,9 +259,8 @@ describe('Quick Filters', () => {
render(<TestQuickFilters />);
// Wait for the filter to load with data
const target = await screen.findByText('mq-kafka', {}, { timeout: 5000 });
// Prefer role if possible; if label text isnt wired to input, clicking the label text is OK
const target = await screen.findByText('mq-kafka');
await user.click(target);
await waitFor(() => {

View File

@@ -29,6 +29,7 @@ export enum LOCALSTORAGE {
DONT_SHOW_SLOW_API_WARNING = 'DONT_SHOW_SLOW_API_WARNING',
METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS',
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
BANNER_DISMISSED = 'BANNER_DISMISSED',
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
FUNNEL_STEPS = 'FUNNEL_STEPS',
SPAN_DETAILS_PINNED_ATTRIBUTES = 'SPAN_DETAILS_PINNED_ATTRIBUTES',

View File

@@ -1,5 +1,4 @@
// ** Helpers
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
@@ -178,7 +177,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
{
metricName: '',
temporality: '',
timeAggregation: MetricAggregateOperator.AVG,
timeAggregation: MetricAggregateOperator.COUNT,
spaceAggregation: MetricAggregateOperator.SUM,
reduceTo: ReduceOperators.AVG,
},
@@ -226,7 +225,7 @@ export const initialQueryBuilderFormMeterValues: IBuilderQuery = {
{
metricName: '',
temporality: '',
timeAggregation: MeterAggregateOperator.AVG,
timeAggregation: MeterAggregateOperator.COUNT,
spaceAggregation: MeterAggregateOperator.SUM,
reduceTo: ReduceOperators.AVG,
},
@@ -372,31 +371,6 @@ export enum ATTRIBUTE_TYPES {
EXPONENTIAL_HISTOGRAM = 'ExponentialHistogram',
}
const METRIC_TYPE_TO_ATTRIBUTE_TYPE: Record<
MetrictypesTypeDTO,
ATTRIBUTE_TYPES
> = {
[MetrictypesTypeDTO.sum]: ATTRIBUTE_TYPES.SUM,
[MetrictypesTypeDTO.gauge]: ATTRIBUTE_TYPES.GAUGE,
[MetrictypesTypeDTO.histogram]: ATTRIBUTE_TYPES.HISTOGRAM,
[MetrictypesTypeDTO.summary]: ATTRIBUTE_TYPES.GAUGE,
[MetrictypesTypeDTO.exponentialhistogram]:
ATTRIBUTE_TYPES.EXPONENTIAL_HISTOGRAM,
};
export function toAttributeType(
metricType: MetrictypesTypeDTO | undefined,
isMonotonic?: boolean,
): ATTRIBUTE_TYPES | '' {
if (!metricType) {
return '';
}
if (metricType === MetrictypesTypeDTO.sum && isMonotonic === false) {
return ATTRIBUTE_TYPES.GAUGE;
}
return METRIC_TYPE_TO_ATTRIBUTE_TYPE[metricType] || '';
}
export type IQueryBuilderState = 'search';
export const QUERY_BUILDER_SEARCH_VALUES = {

View File

@@ -49,6 +49,7 @@ export const REACT_QUERY_KEY = {
// Metrics Explorer Query Keys
GET_METRICS_LIST: 'GET_METRICS_LIST',
GET_METRICS_TREE_MAP: 'GET_METRICS_TREE_MAP',
GET_METRICS_LIST_FILTER_KEYS: 'GET_METRICS_LIST_FILTER_KEYS',
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',

View File

@@ -38,6 +38,7 @@ const ROUTES = {
SETTINGS: '/settings',
MY_SETTINGS: '/settings/my-settings',
ORG_SETTINGS: '/settings/org-settings',
CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings',
API_KEYS: '/settings/api-keys',
INGESTION_SETTINGS: '/settings/ingestion-settings',
SOMETHING_WENT_WRONG: '/something-went-wrong',
@@ -55,7 +56,6 @@ const ROUTES = {
TRACE_EXPLORER: '/trace-explorer',
BILLING: '/settings/billing',
ROLES_SETTINGS: '/settings/roles',
ROLE_DETAILS: '/settings/roles/:roleId',
SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces/saved-views',

View File

@@ -74,7 +74,7 @@ describe('Alert Channels Settings List page', () => {
});
await waitFor(() => {
expect(successNotification).toHaveBeenCalledWith({
expect(successNotification).toBeCalledWith({
message: 'Success',
description: 'channel_delete_success',
});

View File

@@ -441,7 +441,7 @@ describe('Footer utils', () => {
reduceTo: undefined,
spaceAggregation: 'sum',
temporality: undefined,
timeAggregation: 'avg',
timeAggregation: 'count',
},
],
disabled: false,

View File

@@ -1,202 +0,0 @@
import { useEffect, useState } from 'react';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import { DialogWrapper } from '@signozhq/dialog';
import { CircleAlert, CircleCheck, LoaderCircle } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
interface CustomDomainEditModalProps {
isOpen: boolean;
onClose: () => void;
customDomainSubdomain?: string;
dnsSuffix: string;
isLoading: boolean;
updateDomainError: AxiosError<RenderErrorResponseDTO> | null;
onClearError: () => void;
onSubmit: (subdomain: string) => void;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function CustomDomainEditModal({
isOpen,
onClose,
customDomainSubdomain,
dnsSuffix,
isLoading,
updateDomainError,
onClearError,
onSubmit,
}: CustomDomainEditModalProps): JSX.Element {
const [value, setValue] = useState(customDomainSubdomain ?? '');
const [validationError, setValidationError] = useState<string | null>(null);
useEffect(() => {
if (isOpen) {
setValue(customDomainSubdomain ?? '');
}
}, [isOpen, customDomainSubdomain]);
const handleClose = (): void => {
setValidationError(null);
onClearError();
onClose();
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setValue(e.target.value);
setValidationError(null);
onClearError();
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
handleSubmit();
}
};
const handleSubmit = (): void => {
if (!value) {
setValidationError('This field is required');
return;
}
if (value.length < 3) {
setValidationError('Minimum 3 characters required');
return;
}
onSubmit(value);
};
const is409 = updateDomainError?.status === 409;
const apiErrorMessage =
(updateDomainError?.response?.data as RenderErrorResponseDTO)?.error
?.message ?? null;
const errorMessage =
validationError ??
(is409
? apiErrorMessage ??
"You've already updated the custom domain once today. Please contact support."
: apiErrorMessage);
const hasError = Boolean(errorMessage);
const statusIcon = ((): JSX.Element => {
if (isLoading) {
return (
<LoaderCircle size={16} className="animate-spin edit-modal-status-icon" />
);
}
if (hasError) {
return <CircleAlert size={16} color={Color.BG_CHERRY_500} />;
}
return <CircleCheck size={16} color={Color.BG_FOREST_500} />;
})();
return (
<DialogWrapper
className="edit-workspace-modal"
title="Edit Workspace Link"
open={isOpen}
onOpenChange={(open: boolean): void => {
if (!open) {
handleClose();
}
}}
width="base"
>
<div className="edit-workspace-modal-content">
<p className="edit-modal-description">
Enter your preferred subdomain to create a unique URL for your team. Need
help?{' '}
<a
href="https://signoz.io/support"
target="_blank"
rel="noreferrer"
className="edit-modal-link"
>
Contact support.
</a>
</p>
<div className="edit-modal-field">
<label
htmlFor="workspace-url-input"
className={`edit-modal-label${
hasError ? ' edit-modal-label--error' : ''
}`}
>
Workspace URL
</label>
<div
className={`edit-modal-input-wrapper${
hasError ? ' edit-modal-input-wrapper--error' : ''
}`}
>
<div className="edit-modal-input-field">
{statusIcon}
<Input
id="workspace-url-input"
aria-describedby="workspace-url-helper"
aria-invalid={hasError}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
autoFocus
/>
</div>
<div className="edit-modal-input-suffix">{dnsSuffix}</div>
</div>
<span
id="workspace-url-helper"
className={`edit-modal-helper${
hasError ? ' edit-modal-helper--error' : ''
}`}
>
{hasError
? errorMessage
: "To help you easily explore SigNoz, we've selected a tenant sub domain name for you."}
</span>
</div>
<div className="edit-modal-note">
<span className="edit-modal-note-emoji">🚧</span>
<span className="edit-modal-note-text">
Note that your previous URL still remains accessible. Your access
credentials for the new URL remain the same.
</span>
</div>
<div className="edit-modal-footer">
{is409 ? (
<LaunchChatSupport
attributes={{ screen: 'Custom Domain Settings' }}
eventName="Custom Domain Settings: Facing Issues Updating Custom Domain"
message="Hi Team, I need help with updating custom domain"
buttonText="Contact Support"
/>
) : (
<Button
variant="solid"
size="md"
color="primary"
className="edit-modal-apply-btn"
onClick={handleSubmit}
disabled={isLoading}
loading={isLoading}
>
Apply Changes
</Button>
)}
</div>
</div>
</DialogWrapper>
);
}

View File

@@ -1,460 +1,262 @@
.beacon {
position: relative;
display: inline-block;
width: 16px;
height: 16px;
flex-shrink: 0;
&::before {
content: '';
position: absolute;
inset: 1px;
border-radius: 50%;
background: rgba(78, 116, 248, 0.2);
}
&::after {
content: '';
position: absolute;
left: 5px;
top: 5px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--primary);
}
}
.custom-domain-card {
.custom-domain-settings-container {
margin-top: 24px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 24px;
width: 100%;
max-width: 768px;
border-radius: 4px;
border: 1px solid var(--l2-border);
background: var(--l2-background);
overflow: hidden;
&--loading {
padding: var(--padding-3);
.custom-domain-settings-content {
width: calc(100% - 30px);
max-width: 736px;
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-lg);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 28px; /* 155.556% */
letter-spacing: -0.09px;
}
.subtitle {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.custom-domain-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: var(--padding-3);
gap: var(--spacing-6);
.custom-domain-settings-card {
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
.custom-domain-edit-button {
border: 1px solid var(--l3-border);
background: var(--l3-background);
.ant-card-body {
padding: 12px;
&:hover {
background: var(--l3-background-hover);
display: flex;
flex-direction: column;
.custom-domain-settings-content-header {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
.custom-domain-card-info {
display: flex;
flex-direction: column;
gap: var(--spacing-6);
}
.custom-domain-settings-content-body {
margin-top: 12px;
display: flex;
gap: 12px;
.custom-domain-card-name-row {
display: flex;
align-items: center;
gap: var(--spacing-5);
}
align-items: flex-end;
justify-content: space-between;
.custom-domain-card-org-name {
color: var(--l1-foreground);
font-size: var(--paragraph-base-500-font-size);
font-weight: var(--paragraph-base-500-font-weight);
line-height: var(--paragraph-base-500-line-height);
letter-spacing: var(--paragraph-base-500-letter-spacing);
}
.custom-domain-url-edit-btn {
.periscope-btn {
border-radius: 2px;
border: 1px solid var(--Slate-200, #2c3140);
background: var(--Ink-200, #23262e);
}
}
}
.custom-domain-card-meta-row {
display: flex;
align-items: center;
gap: var(--spacing-10);
padding-left: 26px;
}
.custom-domain-urls {
display: flex;
flex-direction: column;
flex: 1;
}
.custom-domain-card-meta-timezone {
display: inline-flex;
align-items: center;
gap: var(--spacing-3);
color: var(--l1-foreground);
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on;
font-family: Inter;
font-size: var(--paragraph-small-400-font-size);
font-style: normal;
font-weight: var(--paragraph-small-400-font-weight);
line-height: var(--paragraph-small-400-line-height);
letter-spacing: var(--paragraph-small-400-letter-spacing);
text-transform: uppercase;
.custom-domain-url {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
svg {
flex-shrink: 0;
color: var(--l1-foreground);
}
}
line-height: 24px;
padding: 4px 0;
}
.custom-domain-callout {
margin: 0 var(--margin-3) var(--margin-3);
font-size: var(--paragraph-base-400-font-size);
max-width: 742px;
--callout-background: var(--primary);
--callout-border-color: var(--callout-primary-border);
--callout-icon-color: var(--primary);
--callout-title-color: var(--callout-primary-title);
}
.custom-domain-update-status {
margin-top: 12px;
.custom-domain-card-divider {
height: 1px;
background: var(--l2-border);
margin: 0;
}
.custom-domain-card-bottom {
display: flex;
align-items: center;
gap: var(--spacing-5);
padding: var(--padding-3);
}
.custom-domain-card-license {
color: var(--l1-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
.custom-domain-plan-badge {
display: inline-flex;
align-items: center;
padding: 0 2px;
border-radius: 2px;
background: var(--l2-background);
color: var(--l2-foreground);
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
}
}
.workspace-url-trigger {
display: inline-flex;
align-items: center;
gap: var(--spacing-3);
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--l1-foreground);
font-size: var(--font-size-xs);
line-height: var(--line-height-18);
letter-spacing: -0.06px;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
svg {
flex-shrink: 0;
color: var(--l2-foreground);
}
}
.workspace-url-dropdown {
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
padding: var(--padding-2) 0;
min-width: 200px;
display: flex;
flex-direction: column;
}
.workspace-url-dropdown-header {
color: var(--l2-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
padding: 0 var(--padding-3) var(--padding-2);
}
.workspace-url-dropdown-divider {
height: 1px;
background: var(--l1-border);
margin-bottom: var(--margin-1);
}
.workspace-url-dropdown-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-4);
padding: 5px var(--padding-3);
cursor: pointer;
text-decoration: none;
transition: background 0.15s ease;
&:hover {
background: var(--l1-background-hover);
.workspace-url-dropdown-item-label {
text-decoration: underline;
}
.workspace-url-dropdown-item-external {
opacity: 1;
}
}
&--active {
background: var(--l1-background-hover);
}
}
.workspace-url-dropdown-item-external {
color: var(--l2-foreground);
flex-shrink: 0;
opacity: 0.5;
transition: opacity 0.15s ease;
}
.workspace-url-dropdown-item-label {
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
color: var(--l2-foreground);
.workspace-url-dropdown-item--active & {
color: var(--l1-foreground);
}
}
.workspace-url-dropdown-item-check {
color: var(--l1-foreground);
flex-shrink: 0;
}
.edit-workspace-modal-content {
display: flex;
flex-direction: column;
gap: var(--spacing-12);
}
// Description
.edit-modal-description {
margin: 0;
color: var(--l1-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
.edit-modal-link {
color: var(--primary);
&:hover {
text-decoration: underline;
}
}
// Input field group
.edit-modal-field {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
.edit-modal-label {
color: var(--l2-foreground);
font-size: var(--paragraph-base-500-font-size);
font-weight: var(--paragraph-base-500-font-weight);
line-height: var(--paragraph-base-500-line-height);
&--error {
color: var(--destructive);
}
}
.edit-modal-input-wrapper {
display: flex;
align-items: stretch;
.edit-modal-input-field {
flex: 1;
display: flex;
align-items: center;
gap: var(--spacing-3);
height: 44px;
padding: 6px var(--padding-3);
background: var(--l1-background);
border: 1px solid var(--l1-border);
border-right: none;
border-radius: 2px 0 0 2px;
svg {
flex-shrink: 0;
}
input {
flex: 1;
width: 100%;
height: auto;
background: transparent;
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
color: var(--l1-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
padding: 0;
&:focus,
&:focus-visible {
outline: none;
box-shadow: none;
color: var(--bg-robin-400);
font-size: 13px;
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
letter-spacing: -0.07px;
border-radius: 4px;
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
}
}
}
}
.edit-modal-input-suffix {
display: flex;
align-items: center;
padding: 6px var(--padding-3);
background: var(--l2-background);
border: 1px solid var(--l1-border);
border-left: none;
border-radius: 0 2px 2px 0;
color: var(--l2-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
white-space: nowrap;
}
.custom-domain-settings-modal {
.ant-modal-content {
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
padding: 0;
.edit-modal-helper {
color: var(--l2-foreground);
font-size: var(--font-size-xs);
line-height: var(--line-height-20);
.ant-modal-header {
background: none;
border-bottom: 1px solid var(--bg-slate-500);
padding: 16px;
margin-bottom: 0;
}
&--error {
color: var(--destructive);
}
}
.ant-modal-close-x {
font-size: 12px;
}
.edit-modal-status-icon {
color: var(--l2-foreground);
}
.ant-modal-body {
padding: 12px 16px;
.edit-modal-note {
display: flex;
gap: var(--spacing-6);
align-items: flex-start;
padding: var(--padding-3);
border-radius: 4px;
background: var(--l2-background);
}
.custom-domain-settings-modal-body {
margin-bottom: 48px;
.edit-modal-note-emoji {
font-size: 16px;
line-height: var(--line-height-20);
flex-shrink: 0;
margin-top: 2px;
}
font-size: 13px;
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.edit-modal-note-text {
color: var(--l2-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
.custom-domain-settings-modal-error {
display: flex;
flex-direction: column;
gap: 24px;
.edit-modal-footer {
.facing-issue-button {
width: 100%;
.update-limit-reached-error {
display: flex;
padding: 20px 24px 24px 24px;
flex-direction: column;
align-items: center;
gap: 24px;
align-self: stretch;
.periscope-btn {
width: 100%;
border-radius: 2px;
background: var(--primary);
border: none;
color: var(--primary-foreground);
font-size: var(--paragraph-base-500-font-size);
font-weight: var(--paragraph-base-500-font-weight);
line-height: var(--paragraph-base-500-line-height);
height: 36px;
border-radius: 4px;
border: 1px solid rgba(255, 205, 86, 0.2);
background: rgba(255, 205, 86, 0.1);
.ant-btn-icon {
display: none;
color: var(--bg-amber-400);
font-size: 13px;
font-style: normal;
line-height: 20px; /* 142.857% */
}
&:hover {
background: var(--primary) !important;
border: none !important;
color: var(--primary-foreground) !important;
.ant-alert-message::first-letter {
text-transform: capitalize;
}
}
.custom-domain-settings-modal-footer {
padding: 16px 0;
margin-top: 0;
display: flex;
justify-content: flex-end;
.apply-changes-btn {
width: 100%;
}
.facing-issue-button {
width: 100%;
.periscope-btn {
width: 100%;
border-radius: 2px;
background: var(--bg-robin-500);
border: none;
color: var(--bg-vanilla-100);
line-height: 20px;
.ant-btn-icon {
display: none;
}
&:hover {
background: var(--bg-robin-500) !important;
border: none !important;
color: var(--bg-vanilla-100) !important;
line-height: 20px !important;
}
}
}
}
}
}
.edit-modal-apply-btn {
width: 100%;
}
.lightMode {
.custom-domain-settings-container {
.custom-domain-settings-content {
.title {
color: var(--bg-ink-400);
}
.custom-domain-toast {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 0 16px;
height: 40px;
width: min(942px, calc(100vw - 32px));
border-radius: 4px;
background: var(--primary-background);
color: var(--bg-base-white);
.subtitle {
color: var(--bg-vanilla-400);
}
}
.custom-domain-toast-message {
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.custom-domain-settings-card {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.custom-domain-toast-actions {
display: flex;
align-items: center;
gap: var(--spacing-4);
flex-shrink: 0;
}
.ant-card-body {
.custom-domain-settings-content-header {
color: var(--bg-ink-100);
}
.custom-domain-toast-visit-btn {
text-decoration: none;
background: var(--bg-robin-600);
.custom-domain-update-status {
color: var(--bg-robin-400);
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
}
&:hover {
background: var(--primary-background-hover);
.custom-domain-url-edit-btn {
.periscope-btn {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
box-shadow: none;
}
}
}
}
}
.custom-domain-toast-dismiss-btn {
color: var(--callout-primary-icon);
height: 24px;
width: 24px;
.custom-domain-settings-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
&:hover {
background: var(--primary-background-hover);
.ant-modal-header {
border-bottom: 1px solid var(--bg-vanilla-300);
}
.custom-domain-settings-modal-error {
.update-limit-reached-error {
border: 1px solid rgba(255, 205, 86, 0.2);
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-500);
}
}
}
}
}

View File

@@ -1,88 +1,59 @@
import { useEffect, useMemo, useState } from 'react';
import { Button } from '@signozhq/button';
import { Callout } from '@signozhq/callout';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import {
Check,
ChevronDown,
Clock,
ExternalLink,
FilePenLine,
Link2,
SolidAlertCircle,
X,
} from '@signozhq/icons';
import { toast } from '@signozhq/sonner';
import { Dropdown, Skeleton } from 'antd';
Alert,
Button,
Card,
Form,
Input,
Modal,
Skeleton,
Tag,
Typography,
} from 'antd';
import {
RenderErrorResponseDTO,
ZeustypesHostDTO,
} from 'api/generated/services/sigNoz.schemas';
import { useGetHosts, usePutHost } from 'api/generated/services/zeus';
import { AxiosError } from 'axios';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useNotifications } from 'hooks/useNotifications';
import { InfoIcon, Link2, Pencil } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
import CustomDomainEditModal from './CustomDomainEditModal';
import './CustomDomainSettings.styles.scss';
function DomainUpdateToast({
toastId,
url,
}: {
toastId: string | number;
url: string;
}): JSX.Element {
const displayUrl = url?.split('://')[1] ?? url;
return (
<div className="custom-domain-toast">
<span className="custom-domain-toast-message">
Your workspace URL is being updated to <strong>{displayUrl}</strong>. This
may take a few minutes.
</span>
<div className="custom-domain-toast-actions">
<Button
variant="ghost"
size="xs"
className="custom-domain-toast-visit-btn"
suffixIcon={<ExternalLink size={12} />}
onClick={(): void => {
window.open(url, '_blank', 'noopener,noreferrer');
}}
>
Visit new URL
</Button>
<Button
variant="ghost"
size="icon"
className="custom-domain-toast-dismiss-btn"
onClick={(): void => {
toast.dismiss(toastId);
}}
aria-label="Dismiss"
prefixIcon={<X size={14} />}
/>
</div>
</div>
);
interface CustomDomainSettingsProps {
subdomain: string;
}
export default function CustomDomainSettings(): JSX.Element {
const { org, activeLicense } = useAppContext();
const { timezone } = useTimezone();
const { org } = useAppContext();
const { notifications } = useNotifications();
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isPollingEnabled, setIsPollingEnabled] = useState(false);
const [hosts, setHosts] = useState<ZeustypesHostDTO[] | null>(null);
const [
updateDomainError,
setUpdateDomainError,
] = useState<AxiosError<RenderErrorResponseDTO> | null>(null);
const [updateDomainError, setUpdateDomainError] = useState<AxiosError | null>(
null,
);
const [customDomainSubdomain, setCustomDomainSubdomain] = useState<
string | undefined
>();
const [, setCopyUrl] = useCopyToClipboard();
const [
customDomainDetails,
setCustomDomainDetails,
] = useState<CustomDomainSettingsProps | null>();
const [editForm] = Form.useForm();
const handleModalClose = (): void => {
setIsEditModalOpen(false);
editForm.resetFields();
setUpdateDomainError(null);
};
const {
data: hostsData,
@@ -96,7 +67,9 @@ export default function CustomDomainSettings(): JSX.Element {
isLoading: isLoadingUpdateCustomDomain,
} = usePutHost<AxiosError<RenderErrorResponseDTO>>();
const stripProtocol = (url: string): string => url?.split('://')[1] ?? url;
const stripProtocol = (url: string): string => {
return url?.split('://')[1] ?? url;
};
const dnsSuffix = useMemo(() => {
const defaultHost = hosts?.find((h) => h.is_default);
@@ -105,11 +78,6 @@ export default function CustomDomainSettings(): JSX.Element {
: '';
}, [hosts]);
const activeHost = useMemo(
() => hosts?.find((h) => !h.is_default) ?? hosts?.find((h) => h.is_default),
[hosts],
);
useEffect(() => {
if (isFetchingHosts || !hostsData) {
return;
@@ -117,14 +85,22 @@ export default function CustomDomainSettings(): JSX.Element {
if (hostsData.status === 'success') {
setHosts(hostsData.data.hosts ?? null);
const customHost = hostsData.data.hosts?.find((h) => !h.is_default);
if (customHost) {
setCustomDomainSubdomain(customHost.name || '');
const activeCustomDomain = hostsData.data.hosts?.find(
(host) => !host.is_default,
);
if (activeCustomDomain) {
setCustomDomainDetails({
subdomain: activeCustomDomain?.name || '',
});
}
}
if (hostsData.data.state !== 'HEALTHY' && isPollingEnabled) {
setTimeout(() => refetchHosts(), 3000);
setTimeout(() => {
refetchHosts();
}, 3000);
}
if (hostsData.data.state === 'HEALTHY') {
@@ -132,175 +108,206 @@ export default function CustomDomainSettings(): JSX.Element {
}
}, [hostsData, refetchHosts, isPollingEnabled, isFetchingHosts]);
const handleSubmit = (subdomain: string): void => {
updateSubDomain(
{ data: { name: subdomain } },
{
onSuccess: () => {
setIsPollingEnabled(true);
refetchHosts();
setIsEditModalOpen(false);
setCustomDomainSubdomain(subdomain);
const newUrl = `https://${subdomain}.${dnsSuffix}`;
toast.custom(
(toastId) => <DomainUpdateToast toastId={toastId} url={newUrl} />,
{ duration: 5000, position: 'bottom-right' }, // this 5 sec is as per design
const onUpdateCustomDomainSettings = (): void => {
editForm
.validateFields()
.then((values) => {
if (values.subdomain) {
updateSubDomain(
{ data: { name: values.subdomain } },
{
onSuccess: () => {
setIsPollingEnabled(true);
refetchHosts();
setIsEditModalOpen(false);
},
onError: (error: AxiosError<RenderErrorResponseDTO>) => {
setUpdateDomainError(error as AxiosError);
setIsPollingEnabled(false);
},
},
);
},
onError: (error: AxiosError<RenderErrorResponseDTO>) => {
setUpdateDomainError(error as AxiosError<RenderErrorResponseDTO>);
setIsPollingEnabled(false);
},
},
);
setCustomDomainDetails({
subdomain: values.subdomain,
});
}
})
.catch((errorInfo) => {
console.error('error info', errorInfo);
});
};
const sortedHosts = useMemo(
() =>
[...(hosts ?? [])].sort((a, b) => {
if (a.name === activeHost?.name) {
return -1;
}
if (b.name === activeHost?.name) {
return 1;
}
if (a.is_default && !b.is_default) {
return 1;
}
if (!a.is_default && b.is_default) {
return -1;
}
return 0;
}),
[hosts, activeHost],
);
const planName = activeLicense?.plan?.name;
if (isLoadingHosts) {
return (
<div className="custom-domain-card custom-domain-card--loading">
<Skeleton
active
title={{ width: '40%' }}
paragraph={{ rows: 1, width: '60%' }}
/>
</div>
);
}
const onCopyUrlHandler = (url: string): void => {
setCopyUrl(stripProtocol(url));
notifications.success({
message: 'Copied to clipboard',
});
};
return (
<>
<div className="custom-domain-card">
<div className="custom-domain-card-top">
<div className="custom-domain-card-info">
<div className="custom-domain-card-name-row">
<span className="beacon" />
<span className="custom-domain-card-org-name">
{org?.[0]?.displayName ? org?.[0]?.displayName : customDomainSubdomain}
</span>
</div>
<div className="custom-domain-card-meta-row">
<Dropdown
trigger={['click']}
dropdownRender={(): JSX.Element => (
<div className="workspace-url-dropdown">
<span className="workspace-url-dropdown-header">
All Workspace URLs
</span>
<div className="workspace-url-dropdown-divider" />
{sortedHosts.map((host) => {
const isActive = host.name === activeHost?.name;
return (
<a
key={host.name}
href={host.url}
target="_blank"
rel="noopener noreferrer"
className={`workspace-url-dropdown-item${
isActive ? ' workspace-url-dropdown-item--active' : ''
}`}
>
<span className="workspace-url-dropdown-item-label">
{stripProtocol(host.url ?? '')}
</span>
{isActive ? (
<Check size={14} className="workspace-url-dropdown-item-check" />
) : (
<ExternalLink
size={12}
className="workspace-url-dropdown-item-external"
/>
)}
</a>
);
})}
</div>
)}
>
<Button
type="button"
size="xs"
className="workspace-url-trigger"
disabled={isFetchingHosts}
>
<Link2 size={12} />
<span>{stripProtocol(activeHost?.url ?? '')}</span>
<ChevronDown size={12} />
</Button>
</Dropdown>
<span className="custom-domain-card-meta-timezone">
<Clock size={11} />
{timezone.offset}
</span>
</div>
</div>
<Button
variant="solid"
size="sm"
className="custom-domain-edit-button"
prefixIcon={<FilePenLine size={12} />}
disabled={isFetchingHosts || isPollingEnabled}
onClick={(): void => setIsEditModalOpen(true)}
>
Edit workspace link
</Button>
</div>
{isPollingEnabled && (
<Callout
type="info"
showIcon
className="custom-domain-callout"
size="small"
icon={<SolidAlertCircle size={13} color="primary" />}
message={`Updating your URL to ⎯ ${customDomainSubdomain}.${dnsSuffix}. This may take a few mins.`}
/>
)}
<div className="custom-domain-card-divider" />
<div className="custom-domain-card-bottom">
<span className="beacon" />
<span className="custom-domain-card-license">
{planName && <code className="custom-domain-plan-badge">{planName}</code>}{' '}
license is currently active
</span>
</div>
<div className="custom-domain-settings-container">
<div className="custom-domain-settings-content">
<header>
<Typography.Title className="title">
Custom Domain Settings
</Typography.Title>
<Typography.Text className="subtitle">
Personalize your workspace domain effortlessly.
</Typography.Text>
</header>
</div>
<CustomDomainEditModal
isOpen={isEditModalOpen}
onClose={(): void => setIsEditModalOpen(false)}
customDomainSubdomain={customDomainSubdomain}
dnsSuffix={dnsSuffix}
isLoading={isLoadingUpdateCustomDomain}
updateDomainError={updateDomainError}
onClearError={(): void => setUpdateDomainError(null)}
onSubmit={handleSubmit}
/>
</>
<div className="custom-domain-settings-content">
{!isLoadingHosts && (
<Card className="custom-domain-settings-card">
<div className="custom-domain-settings-content-header">
Team {org?.[0]?.displayName} Information
</div>
<div className="custom-domain-settings-content-body">
<div className="custom-domain-urls">
{hosts?.map((host) => (
<div
className="custom-domain-url"
key={host.name}
onClick={(): void => onCopyUrlHandler(host.url || '')}
>
<Link2 size={12} /> {stripProtocol(host.url || '')}
{host.is_default && <Tag color={Color.BG_ROBIN_500}>Default</Tag>}
</div>
))}
</div>
<div className="custom-domain-url-edit-btn">
<Button
className="periscope-btn"
disabled={isLoadingHosts || isFetchingHosts || isPollingEnabled}
type="default"
icon={<Pencil size={10} />}
onClick={(): void => setIsEditModalOpen(true)}
>
Customize teams URL
</Button>
</div>
</div>
{isPollingEnabled && (
<Alert
className="custom-domain-update-status"
message={`Updating your URL to ⎯ ${customDomainDetails?.subdomain}.${dnsSuffix}. This may take a few mins.`}
type="info"
icon={<InfoIcon size={12} />}
/>
)}
</Card>
)}
{isLoadingHosts && (
<Card className="custom-domain-settings-card">
<Skeleton
className="custom-domain-settings-skeleton"
active
paragraph={{ rows: 2 }}
/>
</Card>
)}
</div>
{/* Update Custom Domain Modal */}
<Modal
className="custom-domain-settings-modal"
title="Customize your teams URL"
open={isEditModalOpen}
key="edit-custom-domain-settings-modal"
afterClose={handleModalClose}
// closable
onCancel={handleModalClose}
destroyOnClose
footer={null}
>
<Form
name="edit-custom-domain-settings-form"
key={customDomainDetails?.subdomain}
form={editForm}
layout="vertical"
autoComplete="off"
initialValues={{
subdomain: customDomainDetails?.subdomain,
}}
>
{updateDomainError?.status !== 409 && (
<>
<div className="custom-domain-settings-modal-body">
Enter your preferred subdomain to create a unique URL for your team.
Need help? Contact support.
</div>
<Form.Item
name="subdomain"
label="Teams URL subdomain"
rules={[{ required: true }, { type: 'string', min: 3 }]}
>
<Input
addonBefore={updateDomainError && <InfoIcon size={12} color="red" />}
placeholder="Enter Domain"
onChange={(): void => setUpdateDomainError(null)}
addonAfter={dnsSuffix}
autoFocus
/>
</Form.Item>
</>
)}
{updateDomainError && (
<div className="custom-domain-settings-modal-error">
{updateDomainError.status === 409 ? (
<Alert
message={
(updateDomainError?.response?.data as RenderErrorResponseDTO)?.error
?.message ||
'Youve already updated the custom domain once today. To make further changes, please contact our support team for assistance.'
}
type="warning"
className="update-limit-reached-error"
/>
) : (
<Typography.Text type="danger">
{
(updateDomainError?.response?.data as RenderErrorResponseDTO)?.error
?.message
}
</Typography.Text>
)}
</div>
)}
{updateDomainError?.status !== 409 && (
<div className="custom-domain-settings-modal-footer">
<Button
className="periscope-btn primary apply-changes-btn"
onClick={onUpdateCustomDomainSettings}
loading={isLoadingUpdateCustomDomain}
>
Apply Changes
</Button>
</div>
)}
{updateDomainError?.status === 409 && (
<div className="custom-domain-settings-modal-footer">
<LaunchChatSupport
attributes={{
screen: 'Custom Domain Settings',
}}
eventName="Custom Domain Settings: Facing Issues Updating Custom Domain"
message="Hi Team, I need help with updating custom domain"
buttonText="Contact Support"
/>
</div>
)}
</Form>
</Modal>
</div>
);
}

View File

@@ -4,21 +4,6 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import CustomDomainSettings from '../CustomDomainSettings';
jest.mock('components/LaunchChatSupport/LaunchChatSupport', () => ({
__esModule: true,
default: ({ buttonText }: { buttonText?: string }): JSX.Element => (
<button type="button">{buttonText ?? 'Facing issues?'}</button>
),
}));
const mockToastCustom = jest.fn();
jest.mock('@signozhq/sonner', () => ({
toast: {
custom: (...args: unknown[]): unknown => mockToastCustom(...args),
dismiss: jest.fn(),
},
}));
const ZEUS_HOSTS_ENDPOINT = '*/api/v2/zeus/hosts';
const mockHostsResponse: GetHosts200 = {
@@ -43,12 +28,9 @@ const mockHostsResponse: GetHosts200 = {
};
describe('CustomDomainSettings', () => {
afterEach(() => {
server.resetHandlers();
mockToastCustom.mockClear();
});
afterEach(() => server.resetHandlers());
it('renders active host URL in the trigger button', async () => {
it('renders host URLs with protocol stripped and marks the default host', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
@@ -57,11 +39,12 @@ describe('CustomDomainSettings', () => {
render(<CustomDomainSettings />);
// The active host is the non-default one (custom-host)
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await screen.findByText(/custom-host\.test\.cloud/i);
expect(screen.getByText('Default')).toBeInTheDocument();
});
it('opens edit modal when clicking the edit button', async () => {
it('opens edit modal with DNS suffix derived from the default host', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
@@ -71,14 +54,14 @@ describe('CustomDomainSettings', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
screen.getByRole('button', { name: /customize team[']s url/i }),
);
expect(
screen.getByRole('dialog', { name: /edit workspace link/i }),
screen.getByRole('dialog', { name: /customize your team[']s url/i }),
).toBeInTheDocument();
// DNS suffix is the part of the default host URL after the name prefix
expect(screen.getByText('test.cloud')).toBeInTheDocument();
@@ -100,13 +83,12 @@ describe('CustomDomainSettings', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
screen.getByRole('button', { name: /customize team[']s url/i }),
);
// The input is inside the modal — find it by its role
const input = screen.getByRole('textbox');
const input = screen.getByPlaceholderText(/enter domain/i);
await user.clear(input);
await user.type(input, 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
@@ -132,111 +114,15 @@ describe('CustomDomainSettings', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
screen.getByRole('button', { name: /customize team[']s url/i }),
);
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'myteam');
await user.type(screen.getByPlaceholderText(/enter domain/i), 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
expect(
await screen.findByRole('button', { name: /contact support/i }),
).toBeInTheDocument();
});
it('shows validation error when subdomain is less than 3 characters', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'ab');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
expect(
screen.getByText(/minimum 3 characters required/i),
).toBeInTheDocument();
});
it('shows all workspace URLs as links in the dropdown', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
// Open the URL dropdown
await user.click(
screen.getByRole('button', { name: /custom-host\.test\.cloud/i }),
);
// Both host URLs should appear as links in the dropdown
const links = await screen.findAllByRole('link');
const hostLinks = links.filter(
(link) =>
link.getAttribute('href')?.includes('test.cloud') &&
link.getAttribute('target') === '_blank',
);
expect(hostLinks).toHaveLength(2);
// Verify the URLs
const hrefs = hostLinks.map((link) => link.getAttribute('href'));
expect(hrefs).toContain('https://accepted-starfish.test.cloud');
expect(hrefs).toContain('https://custom-host.test.cloud');
});
it('calls toast.custom with new URL after successful domain update', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
rest.put(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({})),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
// Verify toast.custom was called
await waitFor(() => {
expect(mockToastCustom).toHaveBeenCalledTimes(1);
});
// Render the toast element to verify its content
const toastRenderer = mockToastCustom.mock.calls[0][0] as (
id: string,
) => JSX.Element;
const { container } = render(toastRenderer('test-id'));
expect(container).toHaveTextContent(/myteam\.test\.cloud/i);
});
});

View File

@@ -1,4 +1,3 @@
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PrecisionOption } from 'components/Graph/types';
import { LegendConfig, TooltipRenderArgs } from 'lib/uPlotV2/components/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
@@ -9,7 +8,7 @@ interface BaseChartProps {
height: number;
showTooltip?: boolean;
showLegend?: boolean;
timezone?: Timezone;
timezone: string;
canPinTooltip?: boolean;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;

View File

@@ -129,12 +129,12 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
onDestroy={onPlotDestroy}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
timezone={timezone.value}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}
height={containerDimensions.height}
layoutChildren={layoutChildren}
isStackedBarChart={widget.stackedBarChart ?? false}
timezone={timezone}
>
<ContextMenu
coordinates={coordinates}

View File

@@ -5,7 +5,12 @@ import { getInitialStackedBands } from 'container/DashboardContainer/visualizati
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { DrawStyle } from 'lib/uPlotV2/config/types';
import {
DrawStyle,
LineInterpolation,
LineStyle,
VisibilityMode,
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { get } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -58,12 +63,7 @@ export function prepareBarPanelConfig({
const minStepInterval = Math.min(...Object.values(stepIntervals));
const builder = buildBaseConfig({
id: widget.id,
thresholds: widget.thresholds,
yAxisUnit: widget.yAxisUnit,
softMin: widget.softMin ?? undefined,
softMax: widget.softMax ?? undefined,
isLogScale: widget.isLogScale,
widget,
isDarkMode,
onClick,
onDragSelect,
@@ -98,8 +98,14 @@ export function prepareBarPanelConfig({
builder.addSeries({
scaleKey: 'y',
drawStyle: DrawStyle.Bar,
panelType: PANEL_TYPES.BAR,
label: label,
colorMapping: widget.customLegendColors ?? {},
spanGaps: false,
lineStyle: LineStyle.Solid,
lineInterpolation: LineInterpolation.Spline,
showPoints: VisibilityMode.Never,
pointSize: 5,
isDarkMode,
stepInterval: currentStepInterval,
});

View File

@@ -100,7 +100,7 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
syncMode={DashboardCursorSync.Crosshair}
timezone={timezone}
timezone={timezone.value}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}
height={containerDimensions.height}

View File

@@ -154,12 +154,7 @@ export function prepareHistogramPanelConfig({
isDarkMode: boolean;
}): UPlotConfigBuilder {
const builder = buildBaseConfig({
id: widget.id,
thresholds: widget.thresholds,
yAxisUnit: widget.yAxisUnit,
softMin: widget.softMin ?? undefined,
softMax: widget.softMax ?? undefined,
isLogScale: widget.isLogScale,
widget,
isDarkMode,
apiResponse,
panelMode,
@@ -196,8 +191,10 @@ export function prepareHistogramPanelConfig({
builder.addSeries({
label: '',
scaleKey: 'y',
drawStyle: DrawStyle.Histogram,
drawStyle: DrawStyle.Bar,
panelType: PANEL_TYPES.HISTOGRAM,
colorMapping: widget.customLegendColors ?? {},
spanGaps: false,
barWidthFactor: 1,
pointSize: 5,
lineColor: '#3f5ecc',
@@ -219,8 +216,10 @@ export function prepareHistogramPanelConfig({
builder.addSeries({
label: label,
scaleKey: 'y',
drawStyle: DrawStyle.Histogram,
drawStyle: DrawStyle.Bar,
panelType: PANEL_TYPES.HISTOGRAM,
colorMapping: widget.customLegendColors ?? {},
spanGaps: false,
barWidthFactor: 1,
pointSize: 5,
isDarkMode,

View File

@@ -118,7 +118,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
}}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
timezone={timezone}
timezone={timezone.value}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}
height={containerDimensions.height}

View File

@@ -82,12 +82,7 @@ export const prepareUPlotConfig = ({
const minStepInterval = Math.min(...Object.values(stepIntervals));
const builder = buildBaseConfig({
id: widget.id,
thresholds: widget.thresholds,
yAxisUnit: widget.yAxisUnit,
softMin: widget.softMin ?? undefined,
softMax: widget.softMax ?? undefined,
isLogScale: widget.isLogScale,
widget,
isDarkMode,
onClick,
onDragSelect,
@@ -125,6 +120,7 @@ export const prepareUPlotConfig = ({
: VisibilityMode.Never,
pointSize: 5,
isDarkMode,
panelType: PANEL_TYPES.TIME_SERIES,
});
});

View File

@@ -1,11 +1,11 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { STEP_INTERVAL_MULTIPLIER } from 'lib/uPlotV2/constants';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { PanelMode } from '../../types';
import { BaseConfigBuilderProps, buildBaseConfig } from '../baseConfigBuilder';
import { buildBaseConfig } from '../baseConfigBuilder';
jest.mock(
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
@@ -27,25 +27,16 @@ jest.mock('lib/uPlotLib/plugins/onClickPlugin', () => ({
default: jest.fn().mockReturnValue({ name: 'onClickPlugin' }),
}));
const createBaseConfigBuilderProps = (
overrides: Partial<
Pick<
BaseConfigBuilderProps,
'id' | 'yAxisUnit' | 'isLogScale' | 'softMin' | 'softMax' | 'thresholds'
>
> = {},
): Pick<
BaseConfigBuilderProps,
'id' | 'yAxisUnit' | 'isLogScale' | 'softMin' | 'softMax' | 'thresholds'
> => ({
id: 'widget-1',
yAxisUnit: 'ms',
isLogScale: false,
softMin: undefined,
softMax: undefined,
thresholds: [],
...overrides,
});
const createWidget = (overrides: Partial<Widgets> = {}): Widgets =>
({
id: 'widget-1',
yAxisUnit: 'ms',
isLogScale: false,
softMin: undefined,
softMax: undefined,
thresholds: [],
...overrides,
} as Widgets);
const createApiResponse = (
overrides: Partial<MetricRangePayloadProps> = {},
@@ -56,7 +47,7 @@ const createApiResponse = (
} as MetricRangePayloadProps);
const baseProps = {
...createBaseConfigBuilderProps(),
widget: createWidget(),
apiResponse: createApiResponse(),
isDarkMode: true,
panelMode: PanelMode.DASHBOARD_VIEW,
@@ -72,14 +63,14 @@ describe('buildBaseConfig', () => {
expect(typeof builder.getLegendItems).toBe('function');
});
it('configures builder with id and DASHBOARD_VIEW preferences', () => {
it('configures builder with widgetId and DASHBOARD_VIEW preferences', () => {
const builder = buildBaseConfig({
...baseProps,
panelMode: PanelMode.DASHBOARD_VIEW,
...createBaseConfigBuilderProps({ id: 'my-widget' }),
widget: createWidget({ id: 'my-widget' }),
});
expect(builder.getId()).toBe('my-widget');
expect(builder.getWidgetId()).toBe('my-widget');
expect(builder.getShouldSaveSelectionPreference()).toBe(true);
});
@@ -136,7 +127,7 @@ describe('buildBaseConfig', () => {
it('configures log scale on y axis when widget.isLogScale is true', () => {
const builder = buildBaseConfig({
...baseProps,
...createBaseConfigBuilderProps({ isLogScale: true }),
widget: createWidget({ isLogScale: true }),
});
const config = builder.getConfig();
@@ -180,7 +171,7 @@ describe('buildBaseConfig', () => {
it('adds thresholds from widget', () => {
const builder = buildBaseConfig({
...baseProps,
...createBaseConfigBuilderProps({
widget: createWidget({
thresholds: [
{
thresholdValue: 80,
@@ -188,7 +179,7 @@ describe('buildBaseConfig', () => {
thresholdUnit: 'ms',
thresholdLabel: 'High',
},
] as ThresholdProps[],
] as Widgets['thresholds'],
}),
});

View File

@@ -1,6 +1,5 @@
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import onClickPlugin, {
OnClickPluginOpts,
} from 'lib/uPlotLib/plugins/onClickPlugin';
@@ -10,32 +9,28 @@ import {
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { ThresholdsDrawHookOptions } from 'lib/uPlotV2/hooks/types';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { PanelMode } from '../types';
export interface BaseConfigBuilderProps {
id: string;
thresholds?: ThresholdProps[];
widget: Widgets;
apiResponse: MetricRangePayloadProps;
isDarkMode: boolean;
onClick?: OnClickPluginOpts['onClick'];
onDragSelect?: (startTime: number, endTime: number) => void;
timezone?: Timezone;
panelMode?: PanelMode;
panelMode: PanelMode;
panelType: PANEL_TYPES;
minTimeScale?: number;
maxTimeScale?: number;
stepInterval?: number;
isLogScale?: boolean;
yAxisUnit?: string;
softMin?: number;
softMax?: number;
}
export function buildBaseConfig({
id,
widget,
isDarkMode,
onClick,
onDragSelect,
@@ -43,14 +38,9 @@ export function buildBaseConfig({
timezone,
panelMode,
panelType,
thresholds,
minTimeScale,
maxTimeScale,
stepInterval,
isLogScale,
yAxisUnit,
softMin,
softMax,
}: BaseConfigBuilderProps): UPlotConfigBuilder {
const tzDate = timezone
? (timestamp: number): Date =>
@@ -58,27 +48,28 @@ export function buildBaseConfig({
: undefined;
const builder = new UPlotConfigBuilder({
id,
onDragSelect,
widgetId: widget.id,
tzDate,
shouldSaveSelectionPreference: panelMode === PanelMode.DASHBOARD_VIEW,
selectionPreferencesSource: panelMode
? [PanelMode.DASHBOARD_VIEW, PanelMode.STANDALONE_VIEW].includes(panelMode)
? SelectionPreferencesSource.LOCAL_STORAGE
: SelectionPreferencesSource.IN_MEMORY
selectionPreferencesSource: [
PanelMode.DASHBOARD_VIEW,
PanelMode.STANDALONE_VIEW,
].includes(panelMode)
? SelectionPreferencesSource.LOCAL_STORAGE
: SelectionPreferencesSource.IN_MEMORY,
stepInterval,
});
const thresholdOptions: ThresholdsDrawHookOptions = {
scaleKey: 'y',
thresholds: (thresholds || []).map((threshold) => ({
thresholds: (widget.thresholds || []).map((threshold) => ({
thresholdValue: threshold.thresholdValue ?? 0,
thresholdColor: threshold.thresholdColor,
thresholdUnit: threshold.thresholdUnit,
thresholdLabel: threshold.thresholdLabel,
})),
yAxisUnit: yAxisUnit,
yAxisUnit: widget.yAxisUnit,
};
builder.addThresholds(thresholdOptions);
@@ -88,8 +79,8 @@ export function buildBaseConfig({
time: true,
min: minTimeScale,
max: maxTimeScale,
logBase: isLogScale ? 10 : undefined,
distribution: isLogScale
logBase: widget.isLogScale ? 10 : undefined,
distribution: widget.isLogScale
? DistributionType.Logarithmic
: DistributionType.Linear,
});
@@ -100,11 +91,11 @@ export function buildBaseConfig({
time: false,
min: undefined,
max: undefined,
softMin: softMin,
softMax: softMax,
softMin: widget.softMin ?? undefined,
softMax: widget.softMax ?? undefined,
thresholds: thresholdOptions,
logBase: isLogScale ? 10 : undefined,
distribution: isLogScale
logBase: widget.isLogScale ? 10 : undefined,
distribution: widget.isLogScale
? DistributionType.Logarithmic
: DistributionType.Linear,
});
@@ -123,7 +114,7 @@ export function buildBaseConfig({
show: true,
side: 2,
isDarkMode,
isLogScale,
isLogScale: widget.isLogScale,
panelType,
});
@@ -132,8 +123,8 @@ export function buildBaseConfig({
show: true,
side: 3,
isDarkMode,
isLogScale,
yAxisUnit,
isLogScale: widget.isLogScale,
yAxisUnit: widget.yAxisUnit,
panelType,
});

View File

@@ -15,7 +15,7 @@ export const getRandomColor = (): string => {
};
export const DATASOURCE_VS_ROUTES: Record<DataSource, string> = {
[DataSource.METRICS]: ROUTES.METRICS_EXPLORER_EXPLORER,
[DataSource.METRICS]: ROUTES.METRICS_EXPLORER,
[DataSource.TRACES]: ROUTES.TRACES_EXPLORER,
[DataSource.LOGS]: ROUTES.LOGS_EXPLORER,
};

View File

@@ -1,152 +0,0 @@
.general-settings-page {
max-width: 768px;
margin: 0 auto;
padding: var(--padding-8) 0 var(--padding-16);
display: flex;
flex-direction: column;
gap: var(--spacing-12);
}
.general-settings-header {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
.general-settings-title {
font-size: var(--paragraph-medium-500-font-size);
font-weight: var(--paragraph-medium-500-font-weight);
line-height: 32px;
letter-spacing: -0.08px;
color: var(--l1-foreground);
}
.general-settings-subtitle {
font-size: var(--font-size-sm);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
color: var(--l2-foreground);
}
.retention-controls-container {
border: 1px solid var(--l2-border);
border-radius: 4px;
overflow: hidden;
}
.retention-controls-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--padding-4);
height: 50px;
background: var(--l2-background);
border-bottom: 1px solid var(--l2-border);
}
.retention-controls-header-label {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-24);
letter-spacing: 0.48px;
text-transform: uppercase;
color: var(--l2-foreground);
}
.retention-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--padding-4);
height: 52px;
background: var(--l2-background);
& + & {
border-top: 1px solid var(--l1-border);
}
}
.retention-row-label {
display: flex;
align-items: center;
gap: var(--spacing-3);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
color: var(--l1-foreground);
svg {
color: var(--l2-foreground);
flex-shrink: 0;
}
}
.retention-row-controls {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
.retention-input-group {
display: flex;
align-items: flex-start;
// todo: https://github.com/SigNoz/components/issues/116
input[type='number'] {
display: flex;
width: 120px;
height: 32px;
align-items: center;
gap: var(--spacing-2);
border-radius: 2px 0 0 2px;
border: 1px solid var(--l2-border);
background: transparent;
text-align: left;
-moz-appearance: textfield;
appearance: textfield;
color: var(--l1-foreground);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-normal);
line-height: 16px;
box-shadow: none;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
&:disabled {
opacity: 0.8;
cursor: not-allowed;
}
}
.ant-select {
.ant-select-selector {
display: flex;
height: 32px;
padding: 6px 6px 6px 8px;
align-items: center;
gap: var(--spacing-2);
border: 1px solid var(--l2-border);
background: var(--l2-background);
width: 80px;
border-left: none;
}
}
}
.retention-error-text {
font-size: var(--font-size-xs);
color: var(--accent-amber);
font-style: italic;
}
.retention-modal-description {
margin: 0;
color: var(--l1-foreground);
font-size: var(--font-size-sm);
line-height: 22px;
}

View File

@@ -3,20 +3,16 @@ import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { useInterval } from 'react-use';
import { LoadingOutlined } from '@ant-design/icons';
import { Button } from '@signozhq/button';
import { Compass, ScrollText } from '@signozhq/icons';
import { Modal, Spin } from 'antd';
import { Button, Card, Col, Divider, Modal, Row, Spin, Typography } from 'antd';
import setRetentionApi from 'api/settings/setRetention';
import setRetentionApiV2 from 'api/settings/setRetentionV2';
import TextToolTip from 'components/TextToolTip';
import CustomDomainSettings from 'container/CustomDomainSettings';
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { StatusCodes } from 'http-status-codes';
import find from 'lodash-es/find';
import { BarChart2 } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import {
ErrorResponse,
@@ -35,17 +31,13 @@ import {
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
} from 'types/api/settings/getRetention';
import { USER_ROLES } from 'types/roles';
import Retention from './Retention';
import StatusMessage from './StatusMessage';
import { ActionItemsContainer, ErrorText, ErrorTextContainer } from './styles';
import './GeneralSettings.styles.scss';
type NumberOrNull = number | null;
// eslint-disable-next-line sonarjs/cognitive-complexity
function GeneralSettings({
metricsTtlValuesPayload,
tracesTtlValuesPayload,
@@ -463,20 +455,12 @@ function GeneralSettings({
onModalToggleHandler(type);
};
const {
isCloudUser: isCloudUserVal,
isEnterpriseSelfHostedUser,
} = useGetTenantLicense();
const isAdmin = user.role === USER_ROLES.ADMIN;
const showCustomDomainSettings =
(isCloudUserVal || isEnterpriseSelfHostedUser) && isAdmin;
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const renderConfig = [
{
name: 'Metrics',
type: 'metrics',
icon: <BarChart2 size={14} />,
retentionFields: [
{
name: t('total_retention_period'),
@@ -519,7 +503,6 @@ function GeneralSettings({
{
name: 'Traces',
type: 'traces',
icon: <Compass size={14} />,
retentionFields: [
{
name: t('total_retention_period'),
@@ -560,7 +543,6 @@ function GeneralSettings({
{
name: 'Logs',
type: 'logs',
icon: <ScrollText size={14} />,
retentionFields: [
{
name: t('total_retention_period'),
@@ -605,66 +587,69 @@ function GeneralSettings({
) {
return (
<Fragment key={category.name}>
<div className="retention-row">
<span className="retention-row-label">
{category.icon}
{category.name}
</span>
<div className="retention-row-controls">
{category.retentionFields.map((field) => (
<Col xs={22} xl={11} key={category.name} style={{ margin: '0.5rem' }}>
<Card style={{ height: '100%' }}>
<Typography.Title style={{ margin: 0 }} level={3}>
{category.name}
</Typography.Title>
<Divider
style={{
margin: '0.5rem 0',
padding: 0,
opacity: 0.5,
marginBottom: '1rem',
}}
/>
{category.retentionFields.map((retentionField) => (
<Retention
key={field.name}
type={category.type as TTTLType}
text={field.name}
retentionValue={field.value}
setRetentionValue={field.setValue}
hide={!!field.hide}
isS3Field={'isS3Field' in field && !!field.isS3Field}
compact
key={retentionField.name}
text={retentionField.name}
retentionValue={retentionField.value}
setRetentionValue={retentionField.setValue}
hide={!!retentionField.hide}
isS3Field={'isS3Field' in retentionField && retentionField.isS3Field}
/>
))}
{!isCloudUserVal && (
<Button
variant="solid"
size="sm"
color="primary"
onClick={category.save.modalOpen}
disabled={category.save.isDisabled}
>
{category.save.saveButtonText}
</Button>
<>
<ActionItemsContainer>
<Button
type="primary"
onClick={category.save.modalOpen}
disabled={category.save.isDisabled}
>
{category.save.saveButtonText}
</Button>
{category.statusComponent}
</ActionItemsContainer>
<Modal
title={t('retention_confirmation')}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={(): void =>
onModalToggleHandler(category.name.toLowerCase() as TTTLType)
}
onOk={(): Promise<void> =>
onOkHandler(category.name.toLowerCase() as TTTLType)
}
centered
open={category.save.modal}
confirmLoading={category.save.apiLoading}
>
<Typography>
{t('retention_confirmation_description', {
name: category.name.toLowerCase(),
})}
</Typography>
</Modal>
</>
)}
</div>
</div>
{!isCloudUserVal && (
<ActionItemsContainer>{category.statusComponent}</ActionItemsContainer>
)}
{!isCloudUserVal && (
<Modal
title={t('retention_confirmation')}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={(): void =>
onModalToggleHandler(category.name.toLowerCase() as TTTLType)
}
onOk={(): Promise<void> =>
onOkHandler(category.name.toLowerCase() as TTTLType)
}
centered
open={category.save.modal}
confirmLoading={category.save.apiLoading}
>
<p className="retention-modal-description">
{t('retention_confirmation_description', {
name: category.name.toLowerCase(),
})}
</p>
</Modal>
)}
</Card>
</Col>
</Fragment>
);
}
@@ -672,24 +657,9 @@ function GeneralSettings({
});
return (
<div className="general-settings-page">
<div className="general-settings-header">
<span className="general-settings-title">General</span>
<span className="general-settings-subtitle">
Manage your workspace settings.
</span>
</div>
{showCustomDomainSettings && <CustomDomainSettings />}
<div className="retention-controls-container">
<div className="retention-controls-header">
<span className="retention-controls-header-label">Retention Controls</span>
</div>
{renderConfig}
</div>
{(!isCloudUserVal || errorText) && (
<>
{Element}
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
<ErrorTextContainer>
{!isCloudUserVal && (
<TextToolTip
@@ -701,10 +671,12 @@ function GeneralSettings({
)}
{errorText && <ErrorText>{errorText}</ErrorText>}
</ErrorTextContainer>
)}
{isCloudUserVal && <GeneralSettingsCloud />}
</div>
<Row justify="start">{renderConfig}</Row>
{isCloudUserVal && <GeneralSettingsCloud />}
</Col>
</>
);
}

View File

@@ -7,7 +7,6 @@ import {
useRef,
useState,
} from 'react';
import { Input as SignozInput } from '@signozhq/input';
import { Col, Row, Select } from 'antd';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { find } from 'lodash-es';
@@ -35,7 +34,6 @@ function Retention({
text,
hide,
isS3Field = false,
compact = false,
}: RetentionProps): JSX.Element | null {
// Filter available units based on type and field
const availableUnits = useMemo(
@@ -128,27 +126,6 @@ function Retention({
return null;
}
if (compact) {
return (
<div className="retention-input-group">
<SignozInput
type="number"
min={0}
value={selectedValue && selectedValue >= 0 ? selectedValue : ''}
disabled={isCloudUserVal}
onChange={(e): void => onChangeHandler(e, setSelectedValue)}
/>
<Select
value={selectedTimeUnit}
onChange={currentSelectedOption}
disabled={isCloudUserVal}
>
{menuItems}
</Select>
</div>
);
}
return (
<RetentionContainer>
<Row justify="space-between">
@@ -185,11 +162,9 @@ interface RetentionProps {
setRetentionValue: Dispatch<SetStateAction<number | null>>;
hide: boolean;
isS3Field?: boolean;
compact?: boolean;
}
Retention.defaultProps = {
isS3Field: false,
compact: false,
};
export default Retention;

View File

@@ -1,5 +1,4 @@
import setRetentionApiV2 from 'api/settings/setRetentionV2';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import {
fireEvent,
render,
@@ -36,20 +35,14 @@ jest.mock('hooks/useComponentPermission', () => ({
}));
jest.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: jest.fn(() => ({
useGetTenantLicense: (): { isCloudUser: boolean } => ({
isCloudUser: false,
isEnterpriseSelfHostedUser: false,
})),
}),
}));
jest.mock('container/GeneralSettingsCloud', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="general-settings-cloud" />,
}));
jest.mock('container/CustomDomainSettings', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="custom-domain-settings" />,
default: (): null => null,
}));
// Mock data
@@ -100,12 +93,10 @@ const mockDisksWithoutS3: IDiskType[] = [
},
];
const getLogsRow = (): HTMLElement => {
const logsLabel = screen.getByText('Logs');
return logsLabel.closest('.retention-row') as HTMLElement;
};
describe('GeneralSettings - S3 Logs Retention', () => {
const BUTTON_SELECTOR = 'button[type="button"]';
const PRIMARY_BUTTON_CLASS = 'ant-btn-primary';
beforeEach(() => {
jest.clearAllMocks();
(setRetentionApiV2 as jest.Mock).mockResolvedValue({
@@ -130,20 +121,21 @@ describe('GeneralSettings - S3 Logs Retention', () => {
/>,
);
const logsRow = getLogsRow();
expect(logsRow).toBeInTheDocument();
// Find the Logs card
const logsCard = screen.getByText('Logs').closest('.ant-card');
expect(logsCard).toBeInTheDocument();
// Find all inputs in the Logs row - there should be 2 (total retention + S3)
const inputs = logsRow.querySelectorAll('input[type="number"]');
// Find all inputs in the Logs card - there should be 2 (total retention + S3)
const inputs = logsCard?.querySelectorAll('input[type="text"]');
expect(inputs).toHaveLength(2);
// The second input is the S3 retention field
const s3Input = inputs[1] as HTMLInputElement;
const s3Input = inputs?.[1] as HTMLInputElement;
// Find the S3 dropdown (next sibling of the S3 input)
const s3Dropdown = s3Input
?.closest('.retention-row-controls')
?.querySelectorAll('.ant-select-selector')[1] as HTMLElement;
const s3Dropdown = s3Input?.nextElementSibling?.querySelector(
'.ant-select-selector',
) as HTMLElement;
expect(s3Dropdown).toBeInTheDocument();
// Click the S3 dropdown to open it
@@ -163,13 +155,16 @@ describe('GeneralSettings - S3 Logs Retention', () => {
await user.clear(s3Input);
await user.type(s3Input, '5');
// Find the save button in the Logs row
const saveButton = logsRow.querySelector(
'button:not([disabled])',
// Find the save button in the Logs card
const buttons = logsCard?.querySelectorAll(BUTTON_SELECTOR);
// The primary button should be the save button
const saveButton = Array.from(buttons || []).find((btn) =>
btn.className.includes(PRIMARY_BUTTON_CLASS),
) as HTMLButtonElement;
expect(saveButton).toBeInTheDocument();
// Wait for button to be enabled
// Wait for button to be enabled (it should enable after value changes)
await waitFor(() => {
expect(saveButton).not.toBeDisabled();
});
@@ -210,8 +205,8 @@ describe('GeneralSettings - S3 Logs Retention', () => {
);
// Verify S3 field is visible
const logsRow = getLogsRow();
const inputs = logsRow.querySelectorAll('input[type="number"]');
const logsCard = screen.getByText('Logs').closest('.ant-card');
const inputs = logsCard?.querySelectorAll('input[type="text"]');
expect(inputs).toHaveLength(2); // Total + S3
});
});
@@ -232,18 +227,19 @@ describe('GeneralSettings - S3 Logs Retention', () => {
/>,
);
const logsRow = getLogsRow();
expect(logsRow).toBeInTheDocument();
// Find the Logs card
const logsCard = screen.getByText('Logs').closest('.ant-card');
expect(logsCard).toBeInTheDocument();
// Only 1 input should be visible (total retention, no S3)
const inputs = logsRow.querySelectorAll('input[type="number"]');
const inputs = logsCard?.querySelectorAll('input[type="text"]');
expect(inputs).toHaveLength(1);
// Change total retention value
const totalInput = inputs[0] as HTMLInputElement;
const totalInput = inputs?.[0] as HTMLInputElement;
// First, change the dropdown to Days (it defaults to Months)
const totalDropdown = logsRow.querySelector(
const totalDropdown = totalInput?.nextElementSibling?.querySelector(
'.ant-select-selector',
) as HTMLElement;
await user.click(totalDropdown);
@@ -267,12 +263,14 @@ describe('GeneralSettings - S3 Logs Retention', () => {
await user.type(totalInput, '60');
// Find the save button
const saveButton = logsRow.querySelector(
'button:not([disabled])',
const buttons = logsCard?.querySelectorAll(BUTTON_SELECTOR);
const saveButton = Array.from(buttons || []).find((btn) =>
btn.className.includes(PRIMARY_BUTTON_CLASS),
) as HTMLButtonElement;
expect(saveButton).toBeInTheDocument();
// Wait for button to be enabled
// Wait for button to be enabled (ensures all state updates have settled)
await waitFor(() => {
expect(saveButton).not.toBeDisabled();
});
@@ -314,21 +312,22 @@ describe('GeneralSettings - S3 Logs Retention', () => {
/>,
);
const logsRow = getLogsRow();
const inputs = logsRow.querySelectorAll('input[type="number"]');
// Find the Logs card
const logsCard = screen.getByText('Logs').closest('.ant-card');
const inputs = logsCard?.querySelectorAll('input[type="text"]');
// Total retention: 30 days = 1 month (displays as 1 Month)
const totalInput = inputs[0] as HTMLInputElement;
// Total retention: 720 hours = 30 days = 1 month (displays as 1 Month)
const totalInput = inputs?.[0] as HTMLInputElement;
expect(totalInput.value).toBe('1');
// S3 retention: 24 days
const s3Input = inputs[1] as HTMLInputElement;
// S3 retention: 24 day
const s3Input = inputs?.[1] as HTMLInputElement;
expect(s3Input.value).toBe('24');
// Verify dropdowns: total shows Months, S3 shows Days
const dropdowns = logsRow.querySelectorAll('.ant-select-selection-item');
expect(dropdowns[0]).toHaveTextContent('Months');
expect(dropdowns[1]).toHaveTextContent('Days');
const dropdowns = logsCard?.querySelectorAll('.ant-select-selection-item');
expect(dropdowns?.[0]).toHaveTextContent('Months');
expect(dropdowns?.[1]).toHaveTextContent('Days');
});
});
@@ -348,22 +347,24 @@ describe('GeneralSettings - S3 Logs Retention', () => {
/>,
);
const logsRow = getLogsRow();
expect(logsRow).toBeInTheDocument();
// Find the Logs card
const logsCard = screen.getByText('Logs').closest('.ant-card');
expect(logsCard).toBeInTheDocument();
// Find the save button by accessible name within the Logs row
const allSaveButtons = screen.getAllByRole('button', { name: /save/i });
const saveButton = allSaveButtons.find((btn) =>
logsRow.contains(btn),
// Find the save button
const buttons = logsCard?.querySelectorAll(BUTTON_SELECTOR);
const saveButton = Array.from(buttons || []).find((btn) =>
btn.className.includes(PRIMARY_BUTTON_CLASS),
) as HTMLButtonElement;
expect(saveButton).toBeInTheDocument();
// Verify save button is disabled on initial load
// Verify save button is disabled on initial load (no changes, S3 disabled with -1)
expect(saveButton).toBeDisabled();
// Find the total retention input
const inputs = logsRow.querySelectorAll('input[type="number"]');
const totalInput = inputs[0] as HTMLInputElement;
const inputs = logsCard?.querySelectorAll('input[type="text"]');
const totalInput = inputs?.[0] as HTMLInputElement;
// Change total retention value to trigger button enable
await user.clear(totalInput);
@@ -384,62 +385,4 @@ describe('GeneralSettings - S3 Logs Retention', () => {
});
});
});
describe('Cloud User Rendering', () => {
beforeEach(() => {
(useGetTenantLicense as jest.Mock).mockReturnValue({
isCloudUser: true,
isEnterpriseSelfHostedUser: false,
});
});
it('should render CustomDomainSettings and GeneralSettingsCloud for cloud admin', () => {
render(
<GeneralSettings
metricsTtlValuesPayload={mockMetricsRetention}
tracesTtlValuesPayload={mockTracesRetention}
logsTtlValuesPayload={mockLogsRetentionWithS3}
getAvailableDiskPayload={mockDisksWithS3}
metricsTtlValuesRefetch={jest.fn()}
tracesTtlValuesRefetch={jest.fn()}
logsTtlValuesRefetch={jest.fn()}
/>,
);
expect(screen.getByTestId('custom-domain-settings')).toBeInTheDocument();
expect(screen.getByTestId('general-settings-cloud')).toBeInTheDocument();
});
});
describe('Enterprise Self-Hosted User Rendering', () => {
beforeEach(() => {
(useGetTenantLicense as jest.Mock).mockReturnValue({
isCloudUser: false,
isEnterpriseSelfHostedUser: true,
});
});
it('should render CustomDomainSettings but not GeneralSettingsCloud', () => {
render(
<GeneralSettings
metricsTtlValuesPayload={mockMetricsRetention}
tracesTtlValuesPayload={mockTracesRetention}
logsTtlValuesPayload={mockLogsRetentionWithS3}
getAvailableDiskPayload={mockDisksWithS3}
metricsTtlValuesRefetch={jest.fn()}
tracesTtlValuesRefetch={jest.fn()}
logsTtlValuesRefetch={jest.fn()}
/>,
);
expect(screen.getByTestId('custom-domain-settings')).toBeInTheDocument();
expect(
screen.queryByTestId('general-settings-cloud'),
).not.toBeInTheDocument();
// Save buttons should be visible for self-hosted
const saveButtons = screen.getAllByRole('button', { name: /save/i });
expect(saveButtons.length).toBeGreaterThan(0);
});
});
});

View File

@@ -1,11 +1,11 @@
.general-settings-container {
margin: var(--spacing-8) 0px;
margin: 16px 8px;
.ant-card-body {
display: flex;
align-items: center;
gap: var(--spacing-8);
gap: 16px;
padding: 8px;
margin: var(--spacing-8) 0;
margin: 16px 0;
}
}

View File

@@ -39,7 +39,7 @@ const mockProps: WidgetGraphComponentProps = {
columnUnits: {},
description: '',
fillSpans: false,
id: 'w-17f905f6-d355-46bd-a78e-cbc87e6f58cc',
id: '17f905f6-d355-46bd-a78e-cbc87e6f58cc',
mergeAllActiveQueries: false,
nullZeroValues: 'zero',
opacity: '1',

View File

@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { Button } from '@signozhq/button';
import { Skeleton } from 'antd';
import { useEffect, useState } from 'react';
import { Button, Skeleton, Tag, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetHosts } from 'api/generated/services/zeus';
import ROUTES from 'constants/routes';
@@ -30,60 +29,77 @@ function DataSourceInfo({
query: { enabled: isEnabled || false },
});
const activeHost = useMemo(
() =>
hostsData?.data?.hosts?.find((h) => !h.is_default) ??
hostsData?.data?.hosts?.find((h) => h.is_default),
[hostsData],
);
const [url, setUrl] = useState<string>('');
const url = useMemo(() => activeHost?.url?.split('://')[1] ?? '', [
activeHost,
]);
const handleConnect = (): void => {
logEvent('Homepage: Connect dataSource clicked', {});
if (activeLicense && activeLicense.platform === LicensePlatform.CLOUD) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(DOCS_LINKS.ADD_DATA_SOURCE, '_blank', 'noopener noreferrer');
useEffect(() => {
if (hostsData) {
const defaultHost = hostsData?.data.hosts?.find((h) => h.is_default);
if (defaultHost?.url) {
const url = defaultHost?.url?.split('://')[1] ?? '';
setUrl(url);
}
}
};
}, [hostsData]);
const renderNotSendingData = (): JSX.Element => (
<>
<h2 className="welcome-title">
<Typography className="welcome-title">
Hello there, Welcome to your SigNoz workspace
</h2>
</Typography>
<p className="welcome-description">
<Typography className="welcome-description">
Youre not sending any data yet. <br />
SigNoz is so much better with your data start by sending your telemetry
data to SigNoz.
</p>
</Typography>
<Card className="welcome-card">
<Card.Content>
<div className="workspace-ready-container">
<div className="workspace-ready-header">
<span className="workspace-ready-title">
<Typography className="workspace-ready-title">
<img src="/Icons/hurray.svg" alt="hurray" />
Your workspace is ready
</span>
</Typography>
<Button
variant="solid"
color="primary"
size="sm"
type="primary"
className="periscope-btn primary"
prefixIcon={<img src="/Icons/container-plus.svg" alt="plus" />}
onClick={handleConnect}
icon={<img src="/Icons/container-plus.svg" alt="plus" />}
role="button"
tabIndex={0}
onClick={(): void => {
logEvent('Homepage: Connect dataSource clicked', {});
if (
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
}
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
handleConnect();
logEvent('Homepage: Connect dataSource clicked', {});
if (
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
}
}
}}
>
@@ -96,7 +112,13 @@ function DataSourceInfo({
<div className="workspace-url">
<Link2 size={12} />
<span className="workspace-url-text">{url}</span>
<Typography className="workspace-url-text">
{url}
<Tag color="default" className="workspace-url-tag">
default
</Tag>
</Typography>
</div>
</div>
)}
@@ -108,9 +130,9 @@ function DataSourceInfo({
const renderDataReceived = (): JSX.Element => (
<>
<h2 className="welcome-title">
<Typography className="welcome-title">
Hello there, Welcome to your SigNoz workspace
</h2>
</Typography>
{!isError && hostsData && (
<Card className="welcome-card">
@@ -120,7 +142,13 @@ function DataSourceInfo({
<div className="workspace-url">
<Link2 size={12} />
<span className="workspace-url-text">{url}</span>
<Typography className="workspace-url-text">
{url}
<Tag color="default" className="workspace-url-tag">
default
</Tag>
</Typography>
</div>
</div>
</div>

View File

@@ -30,7 +30,7 @@ const mockHostsResponse: GetHosts200 = {
describe('DataSourceInfo', () => {
afterEach(() => server.resetHandlers());
it('renders the active workspace URL with protocol stripped', async () => {
it('renders the default workspace URL with protocol stripped', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
@@ -39,7 +39,7 @@ describe('DataSourceInfo', () => {
render(<DataSourceInfo dataSentToSigNoz={false} isLoading={false} />);
await screen.findByText(/custom-host\.test\.cloud/i);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
});
it('does not render workspace URL when GET /zeus/hosts fails', async () => {
@@ -55,7 +55,7 @@ describe('DataSourceInfo', () => {
expect(screen.queryByText(/signoz\.cloud/i)).not.toBeInTheDocument();
});
it('renders active workspace URL in the data-received view when telemetry is flowing', async () => {
it('renders workspace URL in the data-received view when telemetry is flowing', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
@@ -64,6 +64,6 @@ describe('DataSourceInfo', () => {
render(<DataSourceInfo dataSentToSigNoz={true} isLoading={false} />);
await screen.findByText(/custom-host\.test\.cloud/i);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
});
});

View File

@@ -25,6 +25,51 @@
background: var(--bg-slate-500);
}
.home-container-banner {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 8px 12px;
width: 100%;
background-color: var(--bg-robin-500);
.home-container-banner-close {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--bg-vanilla-100);
position: absolute;
right: 12px;
}
.home-container-banner-content {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
.home-container-banner-link {
color: var(--bg-vanilla-100);
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
text-decoration: underline;
}
}
}
.home-header-left {
display: flex;
align-items: center;
@@ -251,11 +296,19 @@
flex-direction: row;
align-items: center;
gap: 8px;
font-size: var(--paragraph-small-400-font-size);
font-weight: var(--paragraph-small-400-font-weight);
line-height: var(--paragraph-small-400-line-height);
letter-spacing: 0.12px;
color: var(--foreground);
.workspace-url-tag {
font-size: 10px;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: 0.12px;
border-radius: 3px;
border: 1px solid var(--Slate-400, #1d212d);
background: var(--Ink-400, #121317);
color: var(--Vanilla-400, #c0c1c3);
}
}
}

View File

@@ -1,13 +1,13 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { Color } from '@signozhq/design-tokens';
import { Compass, Dot, House, Plus, Wrench } from '@signozhq/icons';
import { Button, Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import listUserPreferences from 'api/v1/user/preferences/list';
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
import Header from 'components/Header/Header';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
@@ -15,8 +15,10 @@ import ROUTES from 'constants/routes';
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
import cloneDeep from 'lodash-es/cloneDeep';
import { CompassIcon, DotIcon, HomeIcon, Plus, Wrench, X } from 'lucide-react';
import { AnimatePresence } from 'motion/react';
import * as motion from 'motion/react-client';
import Card from 'periscope/components/Card/Card';
@@ -49,6 +51,8 @@ export default function Home(): JSX.Element {
const [updatingUserPreferences, setUpdatingUserPreferences] = useState(false);
const [loadingUserPreferences, setLoadingUserPreferences] = useState(true);
const { isCommunityUser, isCommunityEnterpriseUser } = useGetTenantLicense();
const [checklistItems, setChecklistItems] = useState<ChecklistItem[]>(
defaultChecklistItemsState,
);
@@ -57,6 +61,13 @@ export default function Home(): JSX.Element {
false,
);
const [isBannerDismissed, setIsBannerDismissed] = useState(false);
useEffect(() => {
const bannerDismissed = localStorage.getItem(LOCALSTORAGE.BANNER_DISMISSED);
setIsBannerDismissed(bannerDismissed === 'true');
}, []);
useEffect(() => {
const now = new Date();
const startTime = new Date(now.getTime() - homeInterval);
@@ -287,13 +298,44 @@ export default function Home(): JSX.Element {
logEvent('Homepage: Visited', {});
}, []);
const hideBanner = (): void => {
localStorage.setItem(LOCALSTORAGE.BANNER_DISMISSED, 'true');
setIsBannerDismissed(true);
};
const showBanner = useMemo(
() => !isBannerDismissed && (isCommunityUser || isCommunityEnterpriseUser),
[isBannerDismissed, isCommunityUser, isCommunityEnterpriseUser],
);
return (
<div className="home-container">
<div className="sticky-header">
{showBanner && (
<div className="home-container-banner">
<div className="home-container-banner-content">
Big News: SigNoz Community Edition now available with SSO (Google OAuth)
and API keys -
<a
href="https://signoz.io/blog/open-source-signoz-now-available-with-sso-and-api-keys/"
target="_blank"
rel="noreferrer"
className="home-container-banner-link"
>
<i>read more</i>
</a>
</div>
<div className="home-container-banner-close">
<X size={16} onClick={hideBanner} />
</div>
</div>
)}
<Header
leftComponent={
<div className="home-header-left">
<House size={14} /> Home
<HomeIcon size={14} /> Home
</div>
}
rightComponent={
@@ -358,7 +400,7 @@ export default function Home(): JSX.Element {
<div className="active-ingestion-card-content-container">
<div className="active-ingestion-card-content">
<div className="active-ingestion-card-content-icon">
<Dot size={16} color={Color.BG_FOREST_500} />
<DotIcon size={16} color={Color.BG_FOREST_500} />
</div>
<div className="active-ingestion-card-content-description">
@@ -385,7 +427,7 @@ export default function Home(): JSX.Element {
}
}}
>
<Compass size={12} />
<CompassIcon size={12} />
Explore Logs
</div>
</div>
@@ -399,7 +441,7 @@ export default function Home(): JSX.Element {
<div className="active-ingestion-card-content-container">
<div className="active-ingestion-card-content">
<div className="active-ingestion-card-content-icon">
<Dot size={16} color={Color.BG_FOREST_500} />
<DotIcon size={16} color={Color.BG_FOREST_500} />
</div>
<div className="active-ingestion-card-content-description">
@@ -426,7 +468,7 @@ export default function Home(): JSX.Element {
}
}}
>
<Compass size={12} />
<CompassIcon size={12} />
Explore Traces
</div>
</div>
@@ -440,7 +482,7 @@ export default function Home(): JSX.Element {
<div className="active-ingestion-card-content-container">
<div className="active-ingestion-card-content">
<div className="active-ingestion-card-content-icon">
<Dot size={16} color={Color.BG_FOREST_500} />
<DotIcon size={16} color={Color.BG_FOREST_500} />
</div>
<div className="active-ingestion-card-content-description">
@@ -467,7 +509,7 @@ export default function Home(): JSX.Element {
}
}}
>
<Compass size={12} />
<CompassIcon size={12} />
Explore Metrics
</div>
</div>

View File

@@ -190,11 +190,6 @@
.ant-table-cell:nth-child(n + 3) {
padding-right: 24px;
}
.status-header {
display: flex;
align-items: center;
gap: 4px;
}
.memory-usage-header {
display: flex;
align-items: center;

View File

@@ -146,14 +146,7 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
),
},
{
title: (
<div className="status-header">
Status
<Tooltip title="Sent system metrics in last 10 mins">
<InfoCircleOutlined />
</Tooltip>
</div>
),
title: 'Status',
dataIndex: 'active',
key: 'active',
width: 100,

View File

@@ -6,7 +6,6 @@ import { isAxiosError } from 'axios';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import EmptyMetricsSearch from 'container/MetricsExplorer/Explorer/EmptyMetricsSearch';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
@@ -116,34 +115,27 @@ function TimeSeries(): JSX.Element {
setYAxisUnit(value);
};
const hasMetricSelected = useMemo(
() => currentQuery.builder.queryData.some((q) => q.aggregateAttribute?.key),
[currentQuery],
);
return (
<div className="meter-time-series-container">
<BuilderUnitsFilter onChange={onUnitChangeHandler} yAxisUnit={yAxisUnit} />
<div className="time-series-container">
{!hasMetricSelected && <EmptyMetricsSearch />}
{hasMetricSelected &&
responseData.map((datapoint, index) => (
<div
className="time-series-view-panel"
// eslint-disable-next-line react/no-array-index-key
key={index}
>
<TimeSeriesView
isFilterApplied={false}
isError={queries[index].isError}
isLoading={queries[index].isLoading}
data={datapoint}
dataSource={DataSource.METRICS}
yAxisUnit={yAxisUnit}
panelType={PANEL_TYPES.BAR}
/>
</div>
))}
{responseData.map((datapoint, index) => (
<div
className="time-series-view-panel"
// eslint-disable-next-line react/no-array-index-key
key={index}
>
<TimeSeriesView
isFilterApplied={false}
isError={queries[index].isError}
isLoading={queries[index].isLoading}
data={datapoint}
dataSource={DataSource.METRICS}
yAxisUnit={yAxisUnit}
panelType={PANEL_TYPES.BAR}
/>
</div>
))}
</div>
</div>
);

View File

@@ -1,21 +1,13 @@
import { Typography } from 'antd';
import { Empty } from 'antd/lib';
interface EmptyMetricsSearchProps {
hasQueryResult?: boolean;
}
export default function EmptyMetricsSearch({
hasQueryResult,
}: EmptyMetricsSearchProps): JSX.Element {
export default function EmptyMetricsSearch(): JSX.Element {
return (
<div className="empty-metrics-search">
<Empty
description={
<Typography.Title level={5}>
{hasQueryResult
? 'No data'
: 'Select a metric and run a query to see the results'}
Please build and run a valid query to see the result
</Typography.Title>
}
/>

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