mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-26 20:00:33 +01:00
Compare commits
7 Commits
fix/time-p
...
refactor/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c3c21aec1 | ||
|
|
c6db1000d5 | ||
|
|
1ddb6f8647 | ||
|
|
9949a72799 | ||
|
|
02d60e78be | ||
|
|
dd72eaac73 | ||
|
|
e13014baba |
17
.github/workflows/goci.yaml
vendored
17
.github/workflows/goci.yaml
vendored
@@ -123,20 +123,3 @@ jobs:
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate authz
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in authz permissions. Run go run cmd/enterprise/*.go generate authz locally and commit."; exit 1)
|
||||
web-settings:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: self-checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: go-install
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
- name: generate-web-settings
|
||||
run: |
|
||||
go run cmd/enterprise/*.go generate config web-settings
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in web settings schema. Run go run cmd/enterprise/*.go generate config web-settings locally and commit."; exit 1)
|
||||
|
||||
23
.github/workflows/jsci.yaml
vendored
23
.github/workflows/jsci.yaml
vendored
@@ -90,26 +90,3 @@ jobs:
|
||||
run: |
|
||||
cd frontend && pnpm generate:api
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated api clients. Run pnpm generate:api in frontend/ locally and commit."; exit 1)
|
||||
web-settings:
|
||||
if: |
|
||||
github.event_name == 'merge_group' ||
|
||||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: self-checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: node-install
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: install-pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
- name: install-frontend
|
||||
run: cd frontend && pnpm install
|
||||
- name: generate-web-settings
|
||||
run: |
|
||||
cd frontend && pnpm generate:config:web-settings
|
||||
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated web settings types. Run pnpm generate:config:web-settings in frontend/ locally and commit."; exit 1)
|
||||
|
||||
@@ -11,7 +11,7 @@ RUN apk update && \
|
||||
|
||||
|
||||
COPY ./target/${OS}-${TARGETARCH}/signoz-community /root/signoz
|
||||
COPY ./templates /root/templates
|
||||
COPY ./templates/email /root/templates
|
||||
COPY frontend/build/ /etc/signoz/web/
|
||||
|
||||
RUN chmod 755 /root /root/signoz
|
||||
|
||||
@@ -12,7 +12,7 @@ RUN apk update && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY ./target/${OS}-${ARCH}/signoz-community /root/signoz-community
|
||||
COPY ./templates /root/templates
|
||||
COPY ./templates/email /root/templates
|
||||
COPY frontend/build/ /etc/signoz/web/
|
||||
|
||||
RUN chmod 755 /root /root/signoz-community
|
||||
|
||||
@@ -11,7 +11,7 @@ RUN apk update && \
|
||||
|
||||
|
||||
COPY ./target/${OS}-${TARGETARCH}/signoz /root/signoz
|
||||
COPY ./templates /root/templates
|
||||
COPY ./templates/email /root/templates
|
||||
COPY frontend/build/ /etc/signoz/web/
|
||||
|
||||
RUN chmod 755 /root /root/signoz
|
||||
|
||||
@@ -26,7 +26,7 @@ RUN go mod download
|
||||
COPY ./cmd/ ./cmd/
|
||||
COPY ./ee/ ./ee/
|
||||
COPY ./pkg/ ./pkg/
|
||||
COPY ./templates /root/templates
|
||||
COPY ./templates/email /root/templates
|
||||
|
||||
COPY Makefile Makefile
|
||||
RUN TARGET_DIR=/root ARCHS=${TARGETARCH} ZEUS_URL=${ZEUSURL} LICENSE_URL=${ZEUSURL}/api/v1 make go-build-enterprise-race
|
||||
|
||||
@@ -12,7 +12,7 @@ RUN apk update && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY ./target/${OS}-${ARCH}/signoz /root/signoz
|
||||
COPY ./templates /root/templates
|
||||
COPY ./templates/email /root/templates
|
||||
COPY frontend/build/ /etc/signoz/web/
|
||||
|
||||
RUN chmod 755 /root /root/signoz
|
||||
|
||||
@@ -35,7 +35,7 @@ RUN go mod download
|
||||
COPY ./cmd/ ./cmd/
|
||||
COPY ./ee/ ./ee/
|
||||
COPY ./pkg/ ./pkg/
|
||||
COPY ./templates /root/templates
|
||||
COPY ./templates/email /root/templates
|
||||
|
||||
COPY Makefile Makefile
|
||||
RUN TARGET_DIR=/root ARCHS=${TARGETARCH} ZEUS_URL=${ZEUSURL} LICENSE_URL=${ZEUSURL}/api/v1 make go-build-enterprise-race
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/web"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
)
|
||||
|
||||
const webSettingsSchemaPath = "docs/config/web-settings.json"
|
||||
|
||||
func registerGenerateConfig(parentCmd *cobra.Command) {
|
||||
configCmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Generate JSON Schema for config",
|
||||
}
|
||||
|
||||
configCmd.AddCommand(&cobra.Command{
|
||||
Use: "web-settings",
|
||||
Short: "Generate JSON Schema for web settings",
|
||||
RunE: func(currCmd *cobra.Command, args []string) error {
|
||||
return generateWebSettings()
|
||||
},
|
||||
})
|
||||
|
||||
parentCmd.AddCommand(configCmd)
|
||||
}
|
||||
|
||||
func generateWebSettings() error {
|
||||
falseVal := false
|
||||
noAdditional := jsonschema.SchemaOrBool{TypeBoolean: &falseVal}
|
||||
|
||||
reflector := jsonschema.Reflector{}
|
||||
reflector.DefaultOptions = append(reflector.DefaultOptions,
|
||||
jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (bool, error) {
|
||||
if params.Value.Kind() == reflect.Struct {
|
||||
params.Schema.AdditionalProperties = &noAdditional
|
||||
}
|
||||
return false, nil
|
||||
}),
|
||||
jsonschema.InterceptDefName(func(t reflect.Type, defaultDefName string) string {
|
||||
return strings.TrimPrefix(defaultDefName, "Web")
|
||||
}),
|
||||
)
|
||||
|
||||
schema, err := reflector.Reflect(web.Settings{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(schema, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(webSettingsSchemaPath, append(data, '\n'), 0o600)
|
||||
}
|
||||
@@ -17,7 +17,6 @@ func RegisterGenerate(parentCmd *cobra.Command, logger *slog.Logger) {
|
||||
|
||||
registerGenerateOpenAPI(generateCmd)
|
||||
registerGenerateAuthz(generateCmd)
|
||||
registerGenerateConfig(generateCmd)
|
||||
|
||||
parentCmd.AddCommand(generateCmd)
|
||||
}
|
||||
|
||||
@@ -182,11 +182,6 @@ alertmanager:
|
||||
poll_interval: 1m
|
||||
# The URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy). Used for generating relative and absolute links back to Alertmanager itself.
|
||||
external_url: http://localhost:8080
|
||||
# The list of globs from which SigNoz's alertmanager notification templates are loaded (e.g. the email.signoz.html layout).
|
||||
# This mirrors the upstream alertmanager `templates` config option. The upstream default templates (default.tmpl, email.tmpl)
|
||||
# are always loaded from the embedded alertmanager assets, so only SigNoz's own templates need to be listed here.
|
||||
templates:
|
||||
- /opt/signoz/conf/templates/alertmanager/*.gotmpl
|
||||
# The global configuration for the alertmanager. All the exahustive fields can be found in the upstream: https://github.com/prometheus/alertmanager/blob/efa05feffd644ba4accb526e98a8c6545d26a783/config/config.go#L833
|
||||
global:
|
||||
# ResolveTimeout is the time after which an alert is declared resolved if it has not been updated.
|
||||
|
||||
@@ -129,8 +129,6 @@ components:
|
||||
type: string
|
||||
schedule:
|
||||
$ref: '#/components/schemas/AlertmanagertypesSchedule'
|
||||
scope:
|
||||
type: string
|
||||
status:
|
||||
$ref: '#/components/schemas/AlertmanagertypesMaintenanceStatus'
|
||||
updatedAt:
|
||||
@@ -274,8 +272,6 @@ components:
|
||||
type: string
|
||||
schedule:
|
||||
$ref: '#/components/schemas/AlertmanagertypesSchedule'
|
||||
scope:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- schedule
|
||||
@@ -18948,77 +18944,6 @@ paths:
|
||||
summary: Get waterfall view for a trace
|
||||
tags:
|
||||
- tracedetail
|
||||
/api/v4/traces/{traceID}/waterfall:
|
||||
post:
|
||||
deprecated: false
|
||||
description: Returns the waterfall view of spans including all spans if total
|
||||
spans are under a limit, a max count otherwise. Aggregations are dropped compared
|
||||
to v3
|
||||
operationId: GetWaterfallV4
|
||||
parameters:
|
||||
- in: path
|
||||
name: traceID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpantypesPostableWaterfall'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesGettableWaterfallTrace'
|
||||
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
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get waterfall view for a trace
|
||||
tags:
|
||||
- tracedetail
|
||||
/api/v5/query_range:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"required": [
|
||||
"posthog",
|
||||
"appcues"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Appcues": {
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Posthog": {
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"appcues": {
|
||||
"$ref": "#/definitions/Appcues"
|
||||
},
|
||||
"posthog": {
|
||||
"$ref": "#/definitions/Posthog"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
@@ -535,7 +535,7 @@ func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID,
|
||||
func (module *module) provisionDashboards(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, provider cloudintegrationtypes.CloudProviderType, service *cloudintegrationtypes.CloudIntegrationService, serviceDefinition *cloudintegrationtypes.ServiceDefinition) error {
|
||||
// TODO: DB calls are in for loop, can be optimized later.
|
||||
for _, dashboard := range serviceDefinition.Assets.Dashboards {
|
||||
slug := cloudintegrationtypes.CloudIntegrationDashboardSlug(provider, service.Type, dashboard.ID)
|
||||
slug := cloudintegrationtypes.IntegrationDashboardSlug(provider, service.Type, dashboard.ID)
|
||||
|
||||
existing, err := module.store.GetIntegrationDashboardBySlug(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
@@ -562,7 +562,7 @@ func (module *module) provisionDashboards(ctx context.Context, orgID valuer.UUID
|
||||
// deprovisionDashboards deletes all dashboard and integration_dashboard rows for the given service.
|
||||
// make sure to call this within a transaction.
|
||||
func (module *module) deprovisionDashboards(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, serviceID cloudintegrationtypes.ServiceID) error {
|
||||
slugPrefix := cloudintegrationtypes.CloudIntegrationDashboardSlugPrefix(provider, serviceID)
|
||||
slugPrefix := cloudintegrationtypes.IntegrationDashboardSlugPrefix(provider, serviceID)
|
||||
rows, err := module.store.ListIntegrationDashboardsBySlugPrefix(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slugPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -588,7 +588,7 @@ func (module *module) deprovisionDashboards(ctx context.Context, orgID valuer.UU
|
||||
// TODO: remove this hack and send idiomatic response to client.
|
||||
func (module *module) enrichDashboardIDs(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, serviceID cloudintegrationtypes.ServiceID, serviceDefinition *cloudintegrationtypes.ServiceDefinition) error {
|
||||
for i, d := range serviceDefinition.Assets.Dashboards {
|
||||
slug := cloudintegrationtypes.CloudIntegrationDashboardSlug(provider, serviceID, d.ID)
|
||||
slug := cloudintegrationtypes.IntegrationDashboardSlug(provider, serviceID, d.ID)
|
||||
row, err := module.store.GetIntegrationDashboardBySlug(ctx, orgID, cloudintegrationtypes.IntegrationDashboardProviderCloudIntegration, slug)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
@@ -23,6 +24,7 @@ type APIHandlerOptions struct {
|
||||
DataConnector interfaces.Reader
|
||||
UsageManager *usage.Manager
|
||||
IntegrationsController *integrations.Controller
|
||||
CloudIntegrationsController *cloudintegrations.Controller
|
||||
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
||||
GatewayUrl string
|
||||
// Querier Influx Interval
|
||||
@@ -40,6 +42,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
|
||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||
Reader: opts.DataConnector,
|
||||
IntegrationsController: opts.IntegrationsController,
|
||||
CloudIntegrationsController: opts.CloudIntegrationsController,
|
||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||
FluxInterval: opts.FluxInterval,
|
||||
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
|
||||
@@ -88,6 +91,17 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
|
||||
}
|
||||
|
||||
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
|
||||
ah.APIHandler.RegisterCloudIntegrationsRoutes(router, am)
|
||||
|
||||
router.HandleFunc(
|
||||
"/api/v1/cloud-integrations/{cloudProvider}/accounts/generate-connection-params",
|
||||
am.EditAccess(ah.CloudIntegrationsGenerateConnectionParams),
|
||||
).Methods(http.MethodGet)
|
||||
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
|
||||
versionResponse := basemodel.GetVersionResponse{
|
||||
Version: version.Info.Version(),
|
||||
|
||||
@@ -89,15 +89,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
useDashboardV2 := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureUseDashboardV2, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureUseDashboardV2.String()),
|
||||
Active: useDashboardV2,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/opamp"
|
||||
@@ -85,13 +86,20 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
// initiate opamp
|
||||
opAmpModel.Init(signoz.SQLStore, signoz.Instrumentation.Logger(), signoz.Modules.OrgGetter)
|
||||
|
||||
integrationsController, err := integrations.NewController(signoz.SQLStore, signoz.Modules.Dashboard)
|
||||
integrationsController, err := integrations.NewController(signoz.SQLStore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't create integrations controller: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
cloudIntegrationsController, err := cloudintegrations.NewController(signoz.SQLStore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"couldn't create cloud provider integrations controller: %w", err,
|
||||
)
|
||||
}
|
||||
|
||||
// ingestion pipelines manager
|
||||
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
|
||||
signoz.SQLStore,
|
||||
@@ -126,6 +134,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
DataConnector: reader,
|
||||
UsageManager: usageManager,
|
||||
IntegrationsController: integrationsController,
|
||||
CloudIntegrationsController: cloudIntegrationsController,
|
||||
LogsParsingPipelineController: logParsingPipelineController,
|
||||
FluxInterval: config.Querier.FluxInterval,
|
||||
GatewayUrl: config.Gateway.URL.String(),
|
||||
@@ -191,6 +200,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
apiHandler.RegisterRoutes(r, am)
|
||||
apiHandler.RegisterLogsRoutes(r, am)
|
||||
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||
apiHandler.RegisterCloudIntegrationsRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
apiHandler.RegisterInfraMetricsRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
||||
|
||||
@@ -94,19 +94,6 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="application/json" id="signoz-boot-settings">
|
||||
[[.Settings]]
|
||||
</script>
|
||||
<script>
|
||||
try {
|
||||
var _el = document.getElementById('signoz-boot-settings');
|
||||
window.signozBootData = {
|
||||
settings: _el ? JSON.parse(_el.textContent) : null,
|
||||
};
|
||||
} catch (e) {
|
||||
window.signozBootData = { settings: null };
|
||||
}
|
||||
</script>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -148,10 +135,7 @@
|
||||
</script>
|
||||
<script>
|
||||
var APPCUES_APP_ID = '<%- APPCUES_APP_ID %>';
|
||||
var appcuesSettings =
|
||||
((window.signozBootData || {}).settings || {}).appcues || {};
|
||||
var appcuesEnabled = appcuesSettings.enabled !== false;
|
||||
if (APPCUES_APP_ID && appcuesEnabled) {
|
||||
if (APPCUES_APP_ID) {
|
||||
(function (d, t) {
|
||||
var a = d.createElement(t);
|
||||
a.async = 1;
|
||||
|
||||
@@ -47,10 +47,10 @@ const config: Config.InitialOptions = {
|
||||
transformIgnorePatterns: [
|
||||
// @chenglou/pretext is ESM-only; @signozhq/ui pulls it in via text-ellipsis.
|
||||
// Pattern 1: allow .pnpm virtual store through (handled by pattern 2), plus root-level ESM packages.
|
||||
'node_modules/(?!(\\.pnpm|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard)/)',
|
||||
'node_modules/(?!(\\.pnpm|lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou/pretext|@signozhq/design-tokens|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid)/)',
|
||||
// Pattern 2: pnpm virtual store — ignore everything except ESM-only packages.
|
||||
// pnpm encodes scoped packages as @scope+name@version, so match on scope prefix.
|
||||
'node_modules/\\.pnpm/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid|copy-text-to-clipboard)[^/]*/node_modules)',
|
||||
'node_modules/\\.pnpm/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@chenglou|@signozhq|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs|uuid)[^/]*/node_modules)',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
"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:config:web-settings": "json2ts ../docs/config/web-settings.json -o src/types/generated/webSettings.ts --style.useTabs --style.tabWidth=1 --style.singleQuote --bannerComment '/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM docs/config/web-settings.json */'"
|
||||
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
@@ -50,7 +49,7 @@
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/icons": "0.4.0",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/ui": "0.0.22",
|
||||
"@signozhq/ui": "0.0.21",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
@@ -161,8 +160,8 @@
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/d3-hierarchy": "1.1.11",
|
||||
"@types/event-source-polyfill": "^1.0.0",
|
||||
"@types/d3-hierarchy": "1.1.11",
|
||||
"@types/history": "4.7.11",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
@@ -188,7 +187,6 @@
|
||||
"is-ci": "^3.0.1",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-styled-components": "^7.2.0",
|
||||
"json-schema-to-typescript": "^15.0.4",
|
||||
"lint-staged": "^17.0.4",
|
||||
"msw": "1.3.2",
|
||||
"orval": "8.9.1",
|
||||
@@ -243,4 +241,4 @@
|
||||
"tmp": "0.2.4",
|
||||
"vite": "npm:rolldown-vite@7.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
frontend/pnpm-lock.yaml
generated
58
frontend/pnpm-lock.yaml
generated
@@ -77,8 +77,8 @@ importers:
|
||||
specifier: 0.0.2
|
||||
version: 0.0.2(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@signozhq/ui':
|
||||
specifier: 0.0.22
|
||||
version: 0.0.22(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
|
||||
specifier: 0.0.21
|
||||
version: 0.0.21(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: 8.21.3
|
||||
version: 8.21.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -449,9 +449,6 @@ importers:
|
||||
jest-styled-components:
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0(styled-components@5.3.11(react-dom@18.2.0(react@18.2.0))(react-is@19.2.6)(react@18.2.0))
|
||||
json-schema-to-typescript:
|
||||
specifier: ^15.0.4
|
||||
version: 15.0.4
|
||||
lint-staged:
|
||||
specifier: ^17.0.4
|
||||
version: 17.0.4
|
||||
@@ -460,7 +457,7 @@ importers:
|
||||
version: 1.3.2(typescript@5.9.3)
|
||||
orval:
|
||||
specifier: 8.9.1
|
||||
version: 8.9.1(prettier@3.8.3)(typescript@5.9.3)
|
||||
version: 8.9.1(typescript@5.9.3)
|
||||
oxfmt:
|
||||
specifier: 0.47.0
|
||||
version: 0.47.0
|
||||
@@ -548,10 +545,6 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@11.9.3':
|
||||
resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1998,9 +1991,6 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@jsdevtools/ono@7.1.3':
|
||||
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
|
||||
|
||||
'@keyv/bigmap@1.3.1':
|
||||
resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==}
|
||||
engines: {node: '>= 18'}
|
||||
@@ -3279,8 +3269,8 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.2.0
|
||||
|
||||
'@signozhq/ui@0.0.22':
|
||||
resolution: {integrity: sha512-CJDyA4H+uXG/U2/d7/nRMNY6WIW0YWc843mfzUQALjm+xOhbO4T+qt67THjV4s1wTMs1cZLkmScbMddf+hXLIQ==}
|
||||
'@signozhq/ui@0.0.21':
|
||||
resolution: {integrity: sha512-uLM3Vqwxlk2USXbwtb3qRLpjZR9b9QSHFQq/jtcfYNMDmIE/sNjSj0nRkEhX4RqqRgsLRt2PVA33aeWxDOLO3g==}
|
||||
peerDependencies:
|
||||
'@signozhq/icons': 0.3.0
|
||||
react: ^18.2.0
|
||||
@@ -6076,11 +6066,6 @@ packages:
|
||||
json-parse-even-better-errors@2.3.1:
|
||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||
|
||||
json-schema-to-typescript@15.0.4:
|
||||
resolution: {integrity: sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
hasBin: true
|
||||
|
||||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
@@ -7119,11 +7104,6 @@ packages:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
prettier@3.8.3:
|
||||
resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-format@27.5.1:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
@@ -9064,12 +9044,6 @@ snapshots:
|
||||
resize-observer-polyfill: 1.5.1
|
||||
throttle-debounce: 5.0.0
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@11.9.3':
|
||||
dependencies:
|
||||
'@jsdevtools/ono': 7.1.3
|
||||
'@types/json-schema': 7.0.15
|
||||
js-yaml: 4.1.1
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
@@ -10824,8 +10798,6 @@ snapshots:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
optional: true
|
||||
|
||||
'@jsdevtools/ono@7.1.3': {}
|
||||
|
||||
'@keyv/bigmap@1.3.1(keyv@5.6.0)':
|
||||
dependencies:
|
||||
hashery: 1.5.1
|
||||
@@ -12041,7 +12013,7 @@ snapshots:
|
||||
- react-dom
|
||||
- tailwindcss
|
||||
|
||||
'@signozhq/ui@0.0.22(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
|
||||
'@signozhq/ui@0.0.21(@emotion/is-prop-valid@1.2.0)(@signozhq/icons@0.4.0)(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.3(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@chenglou/pretext': 0.0.5
|
||||
'@radix-ui/react-checkbox': 1.3.3(@types/react-dom@18.0.10)(@types/react@18.0.26)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -15402,18 +15374,6 @@ snapshots:
|
||||
|
||||
json-parse-even-better-errors@2.3.1: {}
|
||||
|
||||
json-schema-to-typescript@15.0.4:
|
||||
dependencies:
|
||||
'@apidevtools/json-schema-ref-parser': 11.9.3
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/lodash': 4.17.24
|
||||
is-glob: 4.0.3
|
||||
js-yaml: 4.1.1
|
||||
lodash: 4.18.1
|
||||
minimist: 1.2.8
|
||||
prettier: 3.8.3
|
||||
tinyglobby: 0.2.15
|
||||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
@@ -16330,7 +16290,7 @@ snapshots:
|
||||
strip-ansi: 6.0.1
|
||||
wcwidth: 1.0.1
|
||||
|
||||
orval@8.9.1(prettier@3.8.3)(typescript@5.9.3):
|
||||
orval@8.9.1(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@commander-js/extra-typings': 14.0.0(commander@14.0.2)
|
||||
'@orval/angular': 8.9.1(typescript@5.9.3)
|
||||
@@ -16361,8 +16321,6 @@ snapshots:
|
||||
typedoc: 0.28.19(typescript@5.9.3)
|
||||
typedoc-plugin-coverage: 4.0.2(typedoc@0.28.19(typescript@5.9.3))
|
||||
typedoc-plugin-markdown: 4.11.0(typedoc@0.28.19(typescript@5.9.3))
|
||||
optionalDependencies:
|
||||
prettier: 3.8.3
|
||||
transitivePeerDependencies:
|
||||
- '@faker-js/faker'
|
||||
- supports-color
|
||||
@@ -16623,8 +16581,6 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.8.3: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
@@ -54,12 +54,5 @@
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
|
||||
"MCP_SERVER": "SigNoz | MCP Server",
|
||||
"AI_ASSISTANT": "SigNoz | AI Assistant",
|
||||
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
|
||||
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
|
||||
"ROLE_DETAILS": "SigNoz | Role Details",
|
||||
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
|
||||
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
|
||||
"PUBLIC_DASHBOARD": "SigNoz | Dashboard"
|
||||
"MCP_SERVER": "SigNoz | MCP Server"
|
||||
}
|
||||
|
||||
@@ -77,12 +77,5 @@
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
|
||||
"MCP_SERVER": "SigNoz | MCP Server",
|
||||
"AI_ASSISTANT": "SigNoz | AI Assistant",
|
||||
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
|
||||
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
|
||||
"ROLE_DETAILS": "SigNoz | Role Details",
|
||||
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
|
||||
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
|
||||
"PUBLIC_DASHBOARD": "SigNoz | Dashboard"
|
||||
"MCP_SERVER": "SigNoz | MCP Server"
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import { PreferenceContextProvider } from 'providers/preferences/context/Prefere
|
||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||
import { LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||
import { extractDomain } from 'utils/app';
|
||||
import { bootSettings } from 'utils/bootData';
|
||||
|
||||
import { Home } from './pageComponents';
|
||||
import PrivateRoute from './Private';
|
||||
@@ -333,7 +332,7 @@ function App(): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
if (isCloudUser || isEnterpriseSelfHostedUser) {
|
||||
if (bootSettings.posthog.enabled && process.env.POSTHOG_KEY) {
|
||||
if (process.env.POSTHOG_KEY) {
|
||||
posthog.init(process.env.POSTHOG_KEY, {
|
||||
api_host: 'https://us.i.posthog.com',
|
||||
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
|
||||
|
||||
@@ -225,10 +225,6 @@ export interface AlertmanagertypesPlannedMaintenanceDTO {
|
||||
*/
|
||||
name: string;
|
||||
schedule: AlertmanagertypesScheduleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
scope?: string;
|
||||
status: AlertmanagertypesMaintenanceStatusDTO;
|
||||
/**
|
||||
* @type string
|
||||
@@ -1718,10 +1714,6 @@ export interface AlertmanagertypesPostablePlannedMaintenanceDTO {
|
||||
*/
|
||||
name: string;
|
||||
schedule: AlertmanagertypesScheduleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
scope?: string;
|
||||
}
|
||||
|
||||
export interface AlertmanagertypesPostableRoutePolicyDTO {
|
||||
@@ -9232,17 +9224,6 @@ export type GetWaterfall200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetWaterfallV4PathParameters = {
|
||||
traceID: string;
|
||||
};
|
||||
export type GetWaterfallV4200 = {
|
||||
data: SpantypesGettableWaterfallTraceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type QueryRangeV5200 = {
|
||||
data: Querybuildertypesv5QueryRangeResponseDTO;
|
||||
/**
|
||||
|
||||
@@ -14,8 +14,6 @@ import type {
|
||||
import type {
|
||||
GetWaterfall200,
|
||||
GetWaterfallPathParameters,
|
||||
GetWaterfallV4200,
|
||||
GetWaterfallV4PathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
SpantypesPostableWaterfallDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
@@ -122,102 +120,3 @@ export const useGetWaterfall = <
|
||||
> => {
|
||||
return useMutation(getGetWaterfallMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Returns the waterfall view of spans including all spans if total spans are under a limit, a max count otherwise. Aggregations are dropped compared to v3
|
||||
* @summary Get waterfall view for a trace
|
||||
*/
|
||||
export const getWaterfallV4 = (
|
||||
{ traceID }: GetWaterfallV4PathParameters,
|
||||
spantypesPostableWaterfallDTO?: BodyType<SpantypesPostableWaterfallDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetWaterfallV4200>({
|
||||
url: `/api/v4/traces/${traceID}/waterfall`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: spantypesPostableWaterfallDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetWaterfallV4MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof getWaterfallV4>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallV4PathParameters;
|
||||
data?: BodyType<SpantypesPostableWaterfallDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof getWaterfallV4>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallV4PathParameters;
|
||||
data?: BodyType<SpantypesPostableWaterfallDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['getWaterfallV4'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof getWaterfallV4>>,
|
||||
{
|
||||
pathParams: GetWaterfallV4PathParameters;
|
||||
data?: BodyType<SpantypesPostableWaterfallDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return getWaterfallV4(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type GetWaterfallV4MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getWaterfallV4>>
|
||||
>;
|
||||
export type GetWaterfallV4MutationBody =
|
||||
| BodyType<SpantypesPostableWaterfallDTO>
|
||||
| undefined;
|
||||
export type GetWaterfallV4MutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get waterfall view for a trace
|
||||
*/
|
||||
export const useGetWaterfallV4 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof getWaterfallV4>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallV4PathParameters;
|
||||
data?: BodyType<SpantypesPostableWaterfallDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof getWaterfallV4>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallV4PathParameters;
|
||||
data?: BodyType<SpantypesPostableWaterfallDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getGetWaterfallV4MutationOptions(options));
|
||||
};
|
||||
|
||||
@@ -41,14 +41,22 @@ $item-spacing: 8px;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
height: auto;
|
||||
color: var(--l1-foreground);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
padding: 0;
|
||||
&.ant-input:focus {
|
||||
|
||||
&:focus,
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, Form, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Card, Form } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { X } from '@signozhq/icons';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Input, InputNumber, Popover, Tooltip } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, InputNumber, Popover, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { DefaultOptionType } from 'antd/es/select';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -259,6 +259,14 @@
|
||||
border-left: transparent;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
|
||||
&:focus:not(:focus-visible),
|
||||
&.ant-btn:focus:not(:focus-visible) {
|
||||
border-color: var(--l2-border);
|
||||
border-left-color: transparent;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,5 +292,21 @@
|
||||
.cm-placeholder {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
$add-on-row-height: 38px;
|
||||
|
||||
.periscope-input-with-label {
|
||||
.input {
|
||||
.ant-select {
|
||||
height: $add-on-row-height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-with-label {
|
||||
.input {
|
||||
height: $add-on-row-height;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,23 @@
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.search {
|
||||
input {
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
--input-focus-background: var(--l2-background);
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
--input-font-size: 14px;
|
||||
--input-border-color: var(--l1-border);
|
||||
--input-focus-border-color: var(--primary-background);
|
||||
--input-focus-outline-width: 0;
|
||||
--input-focus-outline-offset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-header-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { Button, Checkbox, Input, Skeleton } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Checkbox, Skeleton } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.duration-input-slider {
|
||||
padding: 12px 0px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
|
||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
@@ -10,17 +9,7 @@ import {
|
||||
CommandShortcut,
|
||||
} from '@signozhq/ui/command';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
AIAssistantEvents,
|
||||
AIAssistantOpenSource,
|
||||
} from 'container/AIAssistant/events';
|
||||
import { normalizePage } from 'container/AIAssistant/hooks/useAIAssistantAnalyticsContext';
|
||||
import {
|
||||
openAIAssistantModal,
|
||||
useAIAssistantStore,
|
||||
} from 'container/AIAssistant/store/useAIAssistantStore';
|
||||
import { useThemeMode } from 'hooks/useDarkMode';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import history from 'lib/history';
|
||||
import { ROLES as UserRole } from 'types/roles';
|
||||
|
||||
@@ -48,11 +37,6 @@ export function CmdKPalette({
|
||||
const { open, setOpen } = useCmdK();
|
||||
|
||||
const { setAutoSwitch, setTheme, theme } = useThemeMode();
|
||||
const location = useLocation();
|
||||
const isAIAssistantEnabled = useIsAIAssistantEnabled();
|
||||
const startNewConversation = useAIAssistantStore(
|
||||
(s) => s.startNewConversation,
|
||||
);
|
||||
|
||||
// toggle palette with ⌘/Ctrl+K
|
||||
function handleGlobalCmdK(
|
||||
@@ -94,21 +78,9 @@ export function CmdKPalette({
|
||||
history.push(key);
|
||||
}
|
||||
|
||||
const handleOpenAIAssistant = (): void => {
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: AIAssistantOpenSource.Cmdk,
|
||||
currentPage: normalizePage(location.pathname),
|
||||
});
|
||||
startNewConversation();
|
||||
openAIAssistantModal();
|
||||
};
|
||||
|
||||
const actions = createShortcutActions({
|
||||
navigate: onClickHandler,
|
||||
handleThemeChange,
|
||||
aiAssistant: isAIAssistantEnabled
|
||||
? { open: handleOpenAIAssistant }
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// RBAC filter: show action if no roles set OR current user role is included
|
||||
|
||||
@@ -10,6 +10,9 @@ export const DEFAULT_AUTH0_APP_REDIRECTION_PATH = ROUTES.APPLICATION;
|
||||
|
||||
export const INVITE_MEMBERS_HASH = '#invite-team-members';
|
||||
|
||||
export const SIGNOZ_UPGRADE_PLAN_URL =
|
||||
'https://upgrade.signoz.io/upgrade-from-app';
|
||||
|
||||
export const DASHBOARD_TIME_IN_DURATION = 'refreshInterval';
|
||||
|
||||
export const DEFAULT_ENTITY_VERSION = 'v3';
|
||||
|
||||
@@ -11,5 +11,4 @@ export enum FeatureKeys {
|
||||
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
|
||||
USE_JSON_BODY = 'use_json_body',
|
||||
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
|
||||
USE_DASHBOARD_V2 = 'use_dashboard_v2',
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
ListMinus,
|
||||
ScrollText,
|
||||
Settings,
|
||||
Sparkles,
|
||||
TowerControl,
|
||||
Workflow,
|
||||
} from '@signozhq/icons';
|
||||
@@ -35,20 +34,12 @@ export type CmdAction = {
|
||||
type ActionDeps = {
|
||||
navigate: (path: string) => void;
|
||||
handleThemeChange: (mode: string) => void;
|
||||
/**
|
||||
* Provided only when the AI Assistant feature is available for the current
|
||||
* tenant. When present, the palette surfaces an "Open AI Assistant" entry
|
||||
* at the top; when absent, the action is omitted entirely.
|
||||
*/
|
||||
aiAssistant?: {
|
||||
open: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
export function createShortcutActions(deps: ActionDeps): CmdAction[] {
|
||||
const { navigate, handleThemeChange, aiAssistant } = deps;
|
||||
const { navigate, handleThemeChange } = deps;
|
||||
|
||||
const actions: CmdAction[] = [
|
||||
return [
|
||||
{
|
||||
id: 'home',
|
||||
name: 'Go to Home',
|
||||
@@ -288,19 +279,4 @@ export function createShortcutActions(deps: ActionDeps): CmdAction[] {
|
||||
perform: (): void => navigate(ROUTES.MEMBERS_SETTINGS),
|
||||
},
|
||||
];
|
||||
|
||||
if (aiAssistant) {
|
||||
actions.unshift({
|
||||
id: 'ai-assistant',
|
||||
name: 'Open AI Assistant',
|
||||
shortcut: ['cmd+j'],
|
||||
keywords: 'ai assistant chat ask sparkles copilot',
|
||||
section: 'AI Assistant',
|
||||
icon: <Sparkles size={14} />,
|
||||
roles: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
perform: aiAssistant.open,
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import logEvent from 'api/common/logEvent';
|
||||
|
||||
import HistorySidebar from '../components/ConversationsList';
|
||||
import ConversationView from '../ConversationView';
|
||||
import { AIAssistantEvents, AIAssistantOpenSource } from '../events';
|
||||
import { AIAssistantEvents } from '../events';
|
||||
import {
|
||||
normalizePage,
|
||||
useAIAssistantAnalyticsContext,
|
||||
@@ -65,7 +65,7 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
startNewConversation();
|
||||
setShowHistory(false);
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: AIAssistantOpenSource.Shortcut,
|
||||
source: 'shortcut',
|
||||
currentPage: normalizePage(pathname),
|
||||
});
|
||||
openModal();
|
||||
@@ -162,57 +162,57 @@ export default function AIAssistantModal(): JSX.Element | null {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={(): void => setShowHistory((v) => !v)}
|
||||
aria-label="Toggle conversations"
|
||||
className={showHistory ? styles.toggleBtnActive : ''}
|
||||
prefix={<History size={14} />}
|
||||
/>
|
||||
>
|
||||
<History size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="New conversation">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={handleNew}
|
||||
aria-label="New conversation"
|
||||
prefix={<Plus size={14} />}
|
||||
/>
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="Open full screen">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={handleExpand}
|
||||
disabled={!activeConversationId}
|
||||
aria-label="Open full screen"
|
||||
prefix={<Maximize2 size={14} />}
|
||||
/>
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="Minimize to side panel">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={handleMinimize}
|
||||
aria-label="Minimize to side panel"
|
||||
prefix={<Minus size={14} />}
|
||||
/>
|
||||
>
|
||||
<Minus size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="Close">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
onClick={closeModal}
|
||||
aria-label="Close"
|
||||
prefix={<X size={14} />}
|
||||
/>
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -150,8 +150,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
color="secondary"
|
||||
onClick={(): void => setShowHistory((v) => !v)}
|
||||
aria-label="Toggle conversations"
|
||||
prefix={<History size={14} />}
|
||||
/>
|
||||
>
|
||||
<History size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="New conversation">
|
||||
@@ -161,8 +162,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
color="secondary"
|
||||
onClick={handleNew}
|
||||
aria-label="New conversation"
|
||||
prefix={<Plus size={14} />}
|
||||
/>
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="Open full screen">
|
||||
@@ -173,8 +175,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
onClick={handleExpand}
|
||||
disabled={!activeConversationId}
|
||||
aria-label="Open full screen"
|
||||
prefix={<Maximize2 size={14} />}
|
||||
/>
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title="Close">
|
||||
@@ -184,8 +187,9 @@ export default function AIAssistantPanel(): JSX.Element | null {
|
||||
color="secondary"
|
||||
onClick={closeDrawer}
|
||||
aria-label="Close panel"
|
||||
prefix={<X size={14} />}
|
||||
/>
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { Bot } from '@signozhq/icons';
|
||||
|
||||
import { AIAssistantEvents, AIAssistantOpenSource } from '../events';
|
||||
import { AIAssistantEvents } from '../events';
|
||||
import { normalizePage } from '../hooks/useAIAssistantAnalyticsContext';
|
||||
import {
|
||||
openAIAssistant,
|
||||
@@ -31,7 +31,7 @@ export default function AIAssistantTrigger(): JSX.Element | null {
|
||||
|
||||
const handleOpen = useCallback((): void => {
|
||||
void logEvent(AIAssistantEvents.Opened, {
|
||||
source: AIAssistantOpenSource.Icon,
|
||||
source: 'icon',
|
||||
currentPage: normalizePage(pathname),
|
||||
});
|
||||
openAIAssistant();
|
||||
|
||||
@@ -159,7 +159,6 @@ export default function ConversationView({
|
||||
<ConversationSkeleton />
|
||||
<div className={inputWrapperClass}>
|
||||
<ChatInput
|
||||
key={conversationId}
|
||||
onSend={handleSend}
|
||||
disabled
|
||||
autoContexts={autoContexts}
|
||||
@@ -173,7 +172,6 @@ export default function ConversationView({
|
||||
return (
|
||||
<div className={styles.conversation}>
|
||||
<VirtualizedMessages
|
||||
key={conversationId}
|
||||
conversationId={conversationId}
|
||||
messages={messages}
|
||||
isStreaming={isStreamingHere}
|
||||
@@ -186,7 +184,6 @@ export default function ConversationView({
|
||||
)}
|
||||
<div className={inputWrapperClass}>
|
||||
<ChatInput
|
||||
key={conversationId}
|
||||
onSend={handleSend}
|
||||
onCancel={handleCancel}
|
||||
disabled={inputDisabled}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
// Collapsed activity summary — one row that hides the underlying
|
||||
// thinking + tool-call steps. Reuses the same quiet treatment as
|
||||
// ThinkingStep / ToolCallStep so it sits flush in the assistant bubble.
|
||||
.activityGroup {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.activityHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--l3-foreground);
|
||||
user-select: none;
|
||||
transition: color 0.12s ease;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.sparkleIcon {
|
||||
flex-shrink: 0;
|
||||
color: var(--accent-primary);
|
||||
|
||||
&.iconPulsing {
|
||||
animation: activityGroupPulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes activityGroupPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.55;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.activitySummary {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.toggleChevron {
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.activityBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2px 0 4px;
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { ChevronDown, ChevronRight, Sparkles } from '@signozhq/icons';
|
||||
|
||||
import { formatTime } from 'utils/timeUtils';
|
||||
|
||||
import { StreamingToolCall } from '../../types';
|
||||
import ThinkingStep, { ThinkingContent, thinkingLabel } from '../ThinkingStep';
|
||||
import ToolCallStep, {
|
||||
getToolDisplayLabel,
|
||||
ToolCallContent,
|
||||
} from '../ToolCallStep';
|
||||
|
||||
import styles from './ActivityGroup.module.scss';
|
||||
|
||||
export type ActivityItem =
|
||||
| { id: string; kind: 'thinking'; content: string }
|
||||
| { id: string; kind: 'tool'; toolCall: StreamingToolCall };
|
||||
|
||||
interface ActivityGroupProps {
|
||||
items: ActivityItem[];
|
||||
/**
|
||||
* True only for the trailing activity group of an active stream — drives
|
||||
* the live "Working…" label and the elapsed-time tick (which re-stamps on
|
||||
* approval/clarification resume so wait time isn't counted).
|
||||
*/
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single-item groups get a step-specific summary so the user doesn't see a
|
||||
* pointless "Worked through 1 step". Multi-item groups roll up into the
|
||||
* generic "Working… / Worked through N steps" treatment.
|
||||
*/
|
||||
function buildSummary(
|
||||
items: ActivityItem[],
|
||||
isLive: boolean,
|
||||
elapsed: number,
|
||||
): string {
|
||||
if (items.length === 1) {
|
||||
const [only] = items;
|
||||
if (only.kind === 'thinking') {
|
||||
return thinkingLabel(isLive);
|
||||
}
|
||||
return getToolDisplayLabel(only.toolCall);
|
||||
}
|
||||
const stepLabel = `${items.length} steps`;
|
||||
if (!isLive) {
|
||||
return `Worked through ${stepLabel}`;
|
||||
}
|
||||
// Suppress the elapsed token until ≥ 1s — the first tick fires after
|
||||
// 1s anyway, and showing "0s" or "<1s" briefly adds noise.
|
||||
return elapsed >= 1000
|
||||
? `Working… · ${formatTime(elapsed / 1000)} · ${stepLabel}`
|
||||
: `Working… · ${stepLabel}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single collapsed summary row that hides a run of thinking + tool-call steps.
|
||||
* Expands to show each underlying step inline. Used for every activity row
|
||||
* (including single-item ones) so all "what the agent did" rows share a
|
||||
* consistent ✨-led visual contract.
|
||||
*/
|
||||
export default function ActivityGroup({
|
||||
items,
|
||||
isLive = false,
|
||||
}: ActivityGroupProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
// Captures the moment this live phase started. Re-stamped on every
|
||||
// false→true transition so a stream that pauses on
|
||||
// approval/clarification and resumes doesn't roll the user's wait time
|
||||
// into the elapsed counter.
|
||||
const startedAtRef = useRef<number>(Date.now());
|
||||
const wasLiveRef = useRef<boolean>(isLive);
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLive && !wasLiveRef.current) {
|
||||
startedAtRef.current = Date.now();
|
||||
setElapsed(0);
|
||||
}
|
||||
wasLiveRef.current = isLive;
|
||||
|
||||
if (!isLive) {
|
||||
return undefined;
|
||||
}
|
||||
// Tick once per second — the display is integer-second precision, so
|
||||
// faster ticks would just re-render the bubble for no visible change.
|
||||
const id = window.setInterval(() => {
|
||||
setElapsed(Date.now() - startedAtRef.current);
|
||||
}, 1000);
|
||||
return (): void => window.clearInterval(id);
|
||||
}, [isLive]);
|
||||
|
||||
const summary = buildSummary(items, isLive, elapsed);
|
||||
const isSingle = items.length === 1;
|
||||
|
||||
const toggle = (): void => setExpanded((v) => !v);
|
||||
|
||||
return (
|
||||
<div className={styles.activityGroup}>
|
||||
<div className={styles.activityHeader} onClick={toggle}>
|
||||
<Sparkles
|
||||
size={12}
|
||||
className={cx(styles.sparkleIcon, { [styles.iconPulsing]: isLive })}
|
||||
/>
|
||||
<span className={styles.activitySummary}>{summary}</span>
|
||||
{expanded ? (
|
||||
<ChevronDown size={12} className={styles.toggleChevron} />
|
||||
) : (
|
||||
<ChevronRight size={12} className={styles.toggleChevron} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className={styles.activityBody}>
|
||||
{isSingle ? (
|
||||
// Single-item: the outer chevron already provides disclosure,
|
||||
// so render the underlying content directly instead of wrapping
|
||||
// it in a second collapsible step row.
|
||||
items[0].kind === 'thinking' ? (
|
||||
<ThinkingContent content={items[0].content} />
|
||||
) : (
|
||||
<ToolCallContent toolCall={items[0].toolCall} />
|
||||
)
|
||||
) : (
|
||||
items.map((item, i) => {
|
||||
// A thinking step is live only while it's the trailing item
|
||||
// in a trailing live group — once any later event (text or
|
||||
// tool) arrives, the pass is done.
|
||||
const isLastItem = i === items.length - 1;
|
||||
return item.kind === 'thinking' ? (
|
||||
<ThinkingStep
|
||||
key={item.id}
|
||||
content={item.content}
|
||||
isLive={isLive && isLastItem}
|
||||
/>
|
||||
) : (
|
||||
<ToolCallStep key={item.id} toolCall={item.toolCall} />
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from './ActivityGroup';
|
||||
export type { ActivityItem } from './ActivityGroup';
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
DialogTitle,
|
||||
} from '@signozhq/ui/dialog';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import type {
|
||||
ApprovalEventDTO,
|
||||
ApprovalEventDTODiff,
|
||||
@@ -101,16 +100,16 @@ export default function ApprovalCard({
|
||||
<div className={styles.diffSection}>
|
||||
<div className={styles.diffHeader}>
|
||||
<span className={styles.diffHeaderLabel}>Diff</span>
|
||||
<TooltipSimple title="Expand diff">
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={(): void => setDiffExpanded(true)}
|
||||
aria-label="Expand diff"
|
||||
prefix={<Maximize2 size={12} />}
|
||||
/>
|
||||
</TooltipSimple>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={(): void => setDiffExpanded(true)}
|
||||
title="Expand diff"
|
||||
aria-label="Expand diff"
|
||||
>
|
||||
<Maximize2 size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
<DiffView diff={approval.diff} />
|
||||
</div>
|
||||
@@ -120,8 +119,6 @@ export default function ApprovalCard({
|
||||
<DialogContent
|
||||
className={styles.diffDialog}
|
||||
style={{ width: '80vw', maxWidth: '80vw', height: '70vh' }}
|
||||
// Skip auto-focus — otherwise the first Copy button opens its tooltip on dialog open.
|
||||
onOpenAutoFocus={(e): void => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Approval diff</DialogTitle>
|
||||
@@ -137,22 +134,19 @@ export default function ApprovalCard({
|
||||
size="sm"
|
||||
value={viewMode}
|
||||
onChange={(next): void => {
|
||||
// Radix `single` group can emit '' when the active item is clicked again.
|
||||
// Radix `single` group can emit '' when the active item
|
||||
// is clicked again — preserve the current mode.
|
||||
if (next === 'split' || next === 'unified') {
|
||||
setViewMode(next);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TooltipSimple title="Split view">
|
||||
<ToggleGroupItem value="split" aria-label="Split view">
|
||||
<Columns2 size={12} />
|
||||
</ToggleGroupItem>
|
||||
</TooltipSimple>
|
||||
<TooltipSimple title="Unified view">
|
||||
<ToggleGroupItem value="unified" aria-label="Unified view">
|
||||
<List size={12} />
|
||||
</ToggleGroupItem>
|
||||
</TooltipSimple>
|
||||
<ToggleGroupItem value="split" aria-label="Split view">
|
||||
<Columns2 size={12} />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="unified" aria-label="Unified view">
|
||||
<List size={12} />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<ToggleGroup
|
||||
type="multiple"
|
||||
@@ -160,16 +154,12 @@ export default function ApprovalCard({
|
||||
value={wrapText ? ['wrap'] : []}
|
||||
onChange={(next): void => setWrapText(next.includes('wrap'))}
|
||||
>
|
||||
<TooltipSimple
|
||||
title={wrapText ? 'Disable text wrap' : 'Wrap long lines'}
|
||||
<ToggleGroupItem
|
||||
value="wrap"
|
||||
aria-label={wrapText ? 'Disable text wrap' : 'Wrap long lines'}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="wrap"
|
||||
aria-label={wrapText ? 'Disable text wrap' : 'Wrap long lines'}
|
||||
>
|
||||
<WrapText size={12} />
|
||||
</ToggleGroupItem>
|
||||
</TooltipSimple>
|
||||
<WrapText size={12} />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
{approval.diff && (
|
||||
@@ -467,16 +457,15 @@ function CopyButton({ text, label }: CopyButtonProps): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipSimple title={copied ? `Copied ${label}` : `Copy ${label}`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={handleCopy}
|
||||
aria-label={copied ? `Copied ${label}` : `Copy ${label}`}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={handleCopy}
|
||||
title={copied ? `Copied ${label}` : `Copy ${label}`}
|
||||
aria-label={copied ? `Copied ${label}` : `Copy ${label}`}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
border-radius: var(--radius-2);
|
||||
padding: 8px;
|
||||
border: 1px solid var(--l1-border);
|
||||
transition: border-color 0.15s;
|
||||
position: relative;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.attachments {
|
||||
@@ -124,18 +129,6 @@
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-2);
|
||||
padding: 4px;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
// Scope the focus ring to the textarea row only — the surrounding
|
||||
// chrome (context chips, "Add Context", mic, send) sits outside this
|
||||
// element and stays unaffected when the cursor enters the textarea.
|
||||
&:focus-within {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 1px
|
||||
color-mix(in srgb, var(--accent-primary), transparent 70%);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
@@ -251,24 +244,16 @@
|
||||
}
|
||||
|
||||
.contextPopoverCategoryItem {
|
||||
// Override DS Button's centered layout.
|
||||
--button-justify-content: flex-start;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
border: 1px solid color-mix(in srgb, var(--l1-foreground), transparent 96%);
|
||||
border-radius: var(--radius-2);
|
||||
font-size: 12px;
|
||||
font-weight: 550;
|
||||
text-align: left;
|
||||
border: 1px solid color-mix(in srgb, var(--l1-foreground), transparent 96%);
|
||||
border-radius: var(--radius-2);
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
@@ -324,24 +309,17 @@
|
||||
}
|
||||
|
||||
.contextPopoverEntityItem {
|
||||
// Override DS Button's centered layout.
|
||||
--button-justify-content: flex-start;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background: transparent;
|
||||
border: 1px solid color-mix(in srgb, var(--l1-foreground), transparent 96%);
|
||||
border-radius: var(--radius-2);
|
||||
color: var(--l1-foreground);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
text-align: left;
|
||||
border: 1px solid color-mix(in srgb, var(--l1-foreground), transparent 96%);
|
||||
border-radius: var(--radius-2);
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
// Required for the inner span's `text-overflow: ellipsis` to engage —
|
||||
// flex items default to `min-width: auto` (intrinsic width) and would
|
||||
@@ -407,11 +385,6 @@
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
// Reset native <button> defaults so the 24px circle isn't inflated by
|
||||
// browser-default padding / font metrics.
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.micDiscard {
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import cx from 'classnames';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
@@ -32,11 +26,7 @@ import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
AIAssistantEvents,
|
||||
VoiceInputSource,
|
||||
getBrowserInfo,
|
||||
} from '../../events';
|
||||
import { AIAssistantEvents, getBrowserInfo } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useSpeechRecognition } from '../../hooks/useSpeechRecognition';
|
||||
import { MessageAttachment } from '../../types';
|
||||
@@ -152,10 +142,6 @@ function autoContextCategory(ctx: MessageContext): string {
|
||||
|
||||
const MAX_INPUT_LENGTH = 20000;
|
||||
const WARNING_THRESHOLD = 15000;
|
||||
// Cap for the auto-growing composer. Past this, the textarea stops growing
|
||||
// and starts scrolling internally so the message list above doesn't get
|
||||
// squeezed in tighter container variants (e.g. the floating panel).
|
||||
const TEXTAREA_MAX_HEIGHT_PX = 200;
|
||||
const HOME_SERVICES_INTERVAL = 30 * 60 * 1000;
|
||||
/** sessionStorage key for the "voice input failed this tab" flag. */
|
||||
const VOICE_UNAVAILABLE_KEY = 'ai-assistant-voice-unavailable';
|
||||
@@ -238,18 +224,6 @@ export default function ChatInput({
|
||||
const [activeContextCategory, setActiveContextCategory] =
|
||||
useState<ContextCategory>('Dashboards');
|
||||
const [pickerSearchQuery, setPickerSearchQuery] = useState('');
|
||||
// Refs to each category tab so we can move DOM focus to the newly-active
|
||||
// tab on ArrowUp/ArrowDown. Without this the roving-tabindex pattern
|
||||
// stalls: focus stays on the original button (whose closure has the old
|
||||
// category), so subsequent arrow keys never advance past the second tab.
|
||||
const categoryTabRefs = useRef(
|
||||
new Map<ContextCategory, HTMLButtonElement | null>(),
|
||||
);
|
||||
// Refs to each entity row in the active tab panel, so we can cross from
|
||||
// the category tablist (ArrowRight) into the panel and step through
|
||||
// entities with ArrowUp/Down. Array is rewritten each render — there's
|
||||
// only ever one tab panel mounted so stale indices clear naturally.
|
||||
const entityRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// When the picker was opened by typing `@` in the textarea, this holds the
|
||||
@@ -329,92 +303,11 @@ export default function ChatInput({
|
||||
[mentionRange, selectedContexts, text],
|
||||
);
|
||||
|
||||
const focusCategory = useCallback((category: ContextCategory) => {
|
||||
setActiveContextCategory(category);
|
||||
setPickerSearchQuery('');
|
||||
categoryTabRefs.current.get(category)?.focus();
|
||||
}, []);
|
||||
|
||||
const handleCategoryKeyDown = useCallback(
|
||||
(
|
||||
e: React.KeyboardEvent<HTMLButtonElement>,
|
||||
category: ContextCategory,
|
||||
): void => {
|
||||
const total = CONTEXT_CATEGORIES.length;
|
||||
const idx = CONTEXT_CATEGORIES.indexOf(category);
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
focusCategory(CONTEXT_CATEGORIES[(idx + 1) % total]);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
focusCategory(CONTEXT_CATEGORIES[(idx - 1 + total) % total]);
|
||||
} else if (e.key === 'Home') {
|
||||
e.preventDefault();
|
||||
focusCategory(CONTEXT_CATEGORIES[0]);
|
||||
} else if (e.key === 'End') {
|
||||
e.preventDefault();
|
||||
focusCategory(CONTEXT_CATEGORIES[total - 1]);
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
// Cross from tablist into entity panel.
|
||||
e.preventDefault();
|
||||
entityRefs.current[0]?.focus();
|
||||
}
|
||||
},
|
||||
[focusCategory],
|
||||
);
|
||||
|
||||
const handleEntityKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLButtonElement>, index: number): void => {
|
||||
const count = entityRefs.current.length;
|
||||
if (count === 0) {
|
||||
return;
|
||||
}
|
||||
const focusAt = (i: number): void => {
|
||||
e.preventDefault();
|
||||
entityRefs.current[i]?.focus();
|
||||
};
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
focusAt((index + 1) % count);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
focusAt((index - 1 + count) % count);
|
||||
break;
|
||||
case 'Home':
|
||||
focusAt(0);
|
||||
break;
|
||||
case 'End':
|
||||
focusAt(count - 1);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
// Cross back to tablist.
|
||||
e.preventDefault();
|
||||
categoryTabRefs.current.get(activeContextCategory)?.focus();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
[activeContextCategory],
|
||||
);
|
||||
|
||||
// Focus the textarea when this component mounts (panel/modal open)
|
||||
useEffect(() => {
|
||||
textareaRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Auto-grow the textarea so long prompts aren't trapped in a 2-line
|
||||
// scrolling porthole. Reset to `auto` first to let the field shrink back
|
||||
// down when the user deletes content, then snap to scrollHeight capped at
|
||||
// TEXTAREA_MAX_HEIGHT_PX (overflow-y: auto in CSS handles the rest).
|
||||
useLayoutEffect(() => {
|
||||
const el = textareaRef.current;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
el.style.height = 'auto';
|
||||
el.style.height = `${Math.min(el.scrollHeight, TEXTAREA_MAX_HEIGHT_PX)}px`;
|
||||
}, [text]);
|
||||
|
||||
const handleSend = useCallback(async () => {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed && pendingFiles.length === 0) {
|
||||
@@ -489,7 +382,7 @@ export default function ChatInput({
|
||||
// start time so we can attribute `durationMs` on the Voice input used
|
||||
// event regardless of which control ended the session.
|
||||
const voiceStartedAtRef = useRef<number | null>(null);
|
||||
const voiceSourceRef = useRef<VoiceInputSource | null>(null);
|
||||
const voiceSourceRef = useRef<'button' | 'shortcut' | null>(null);
|
||||
// Set to true after a `network`, `not-allowed`, or `not-supported` failure
|
||||
// so we hide the mic button for the rest of the tab session — silent
|
||||
// retries don't help, and Chromium derivatives without the Google Speech
|
||||
@@ -566,7 +459,7 @@ export default function ChatInput({
|
||||
const showMic = isSupported && micPermission !== 'denied' && !voiceUnavailable;
|
||||
|
||||
const startVoiceInput = useCallback(
|
||||
(source: VoiceInputSource) => {
|
||||
(source: 'button' | 'shortcut') => {
|
||||
// Defense in depth: the button is hidden when `voiceUnavailable` is
|
||||
// true, but the PTT shortcut listener can still call us. Bailing here
|
||||
// keeps a single source of truth and prevents repeat `Voice input
|
||||
@@ -643,7 +536,7 @@ export default function ChatInput({
|
||||
return; // ignore auto-repeat
|
||||
}
|
||||
pttActiveRef.current = true;
|
||||
startVoiceInput(VoiceInputSource.Shortcut);
|
||||
startVoiceInput('shortcut');
|
||||
};
|
||||
|
||||
const handleKeyUp = (e: KeyboardEvent): void => {
|
||||
@@ -831,12 +724,6 @@ export default function ChatInput({
|
||||
entity.value.toLowerCase().includes(activeQuery),
|
||||
)
|
||||
: contextEntitiesByCategory[activeContextCategory];
|
||||
// Truncate the ref array to match the current entity count so that
|
||||
// switching from a large category (e.g. 100 dashboards) to a smaller one
|
||||
// doesn't leave stale `null` slots from earlier renders. Keyboard nav math
|
||||
// already uses `filteredContextOptions.length` for the modulo, so stale
|
||||
// slots wouldn't be reached — this is purely housekeeping.
|
||||
entityRefs.current.length = filteredContextOptions.length;
|
||||
const { isLoading: isActiveContextLoading, isError: isActiveContextError } =
|
||||
contextCategoryStateByCategory[activeContextCategory];
|
||||
const currentLength = text.length;
|
||||
@@ -943,7 +830,7 @@ export default function ChatInput({
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={disabled}
|
||||
maxLength={MAX_INPUT_LENGTH}
|
||||
rows={3}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
{showTextWarning && (
|
||||
@@ -990,37 +877,15 @@ export default function ChatInput({
|
||||
sideOffset={8}
|
||||
>
|
||||
<div className={styles.contextPopoverContent}>
|
||||
<div
|
||||
className={styles.contextPopoverCategories}
|
||||
role="tablist"
|
||||
aria-orientation="vertical"
|
||||
aria-label="Context categories"
|
||||
>
|
||||
<div className={styles.contextPopoverCategories}>
|
||||
{CONTEXT_CATEGORIES.map((category) => {
|
||||
const CategoryIcon = CONTEXT_CATEGORY_ICONS[category];
|
||||
const isActive = activeContextCategory === category;
|
||||
return (
|
||||
<Button
|
||||
<div
|
||||
key={category}
|
||||
ref={(el): void => {
|
||||
categoryTabRefs.current.set(category, el);
|
||||
}}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
role="tab"
|
||||
id={`ai-context-tab-${category}`}
|
||||
// Single stable panel id shared by every tab: only the
|
||||
// active category's tabpanel is rendered, so per-category
|
||||
// `aria-controls` ids would point at nonexistent nodes
|
||||
// for the two inactive tabs. APG allows a single
|
||||
// dynamic panel whose `aria-labelledby` swaps to the
|
||||
// active tab.
|
||||
aria-controls="ai-context-tabpanel"
|
||||
// Roving tabindex: only the active tab participates in
|
||||
// the Tab sequence; arrow keys move between tabs.
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
tabIndex={0}
|
||||
aria-selected={isActive}
|
||||
className={cx(styles.contextPopoverCategoryItem, {
|
||||
[styles.active]: isActive,
|
||||
@@ -1029,21 +894,22 @@ export default function ChatInput({
|
||||
setActiveContextCategory(category);
|
||||
setPickerSearchQuery('');
|
||||
}}
|
||||
onKeyDown={(e): void => handleCategoryKeyDown(e, category)}
|
||||
prefix={<CategoryIcon size={13} />}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setActiveContextCategory(category);
|
||||
setPickerSearchQuery('');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CategoryIcon size={13} />
|
||||
<span>{category}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.contextPopoverRight}
|
||||
role="tabpanel"
|
||||
id="ai-context-tabpanel"
|
||||
aria-labelledby={`ai-context-tab-${activeContextCategory}`}
|
||||
>
|
||||
<div className={styles.contextPopoverRight}>
|
||||
<div className={styles.contextPopoverSearch}>
|
||||
<Input
|
||||
type="text"
|
||||
@@ -1073,7 +939,7 @@ export default function ChatInput({
|
||||
No matching entities
|
||||
</div>
|
||||
) : (
|
||||
filteredContextOptions.map((option, index) => {
|
||||
filteredContextOptions.map((option) => {
|
||||
const isSelected = selectedContexts.some(
|
||||
(item) =>
|
||||
item.category === activeContextCategory &&
|
||||
@@ -1081,16 +947,8 @@ export default function ChatInput({
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
<div
|
||||
key={option.id}
|
||||
ref={(el): void => {
|
||||
entityRefs.current[index] = el;
|
||||
}}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
aria-pressed={isSelected}
|
||||
className={cx(styles.contextPopoverEntityItem, {
|
||||
[styles.selected]: isSelected,
|
||||
})}
|
||||
@@ -1101,12 +959,11 @@ export default function ChatInput({
|
||||
option.value,
|
||||
)
|
||||
}
|
||||
onKeyDown={(e): void => handleEntityKeyDown(e, index)}
|
||||
>
|
||||
<span className={styles.contextPopoverEntityItemText}>
|
||||
{option.value}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
@@ -1120,24 +977,14 @@ export default function ChatInput({
|
||||
<div className={styles.rightActions}>
|
||||
{showMic &&
|
||||
(isListening ? (
|
||||
<div
|
||||
className={styles.micRecording}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="Recording voice input"
|
||||
>
|
||||
<TooltipSimple title="Discard recording">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className={cx(styles.micDiscard, styles.secondary)}
|
||||
onClick={handleDiscard}
|
||||
aria-label="Discard recording"
|
||||
prefix={<X size={12} />}
|
||||
/>
|
||||
</TooltipSimple>
|
||||
<div className={styles.micRecording}>
|
||||
<div
|
||||
className={cx(styles.micDiscard, styles.secondary)}
|
||||
onClick={handleDiscard}
|
||||
aria-label="Discard recording"
|
||||
>
|
||||
<X size={12} />
|
||||
</div>
|
||||
<span className={styles.micWaves} aria-hidden="true">
|
||||
<span />
|
||||
<span />
|
||||
@@ -1148,30 +995,26 @@ export default function ChatInput({
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
<TooltipSimple title="Stop and send">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="destructive"
|
||||
className={cx(styles.micStop, styles.destructive)}
|
||||
onClick={handleStopAndSend}
|
||||
aria-label="Stop and send"
|
||||
prefix={<Square size={9} fill="currentColor" strokeWidth={0} />}
|
||||
/>
|
||||
</TooltipSimple>
|
||||
<div
|
||||
className={cx(styles.micStop, styles.destructive)}
|
||||
onClick={handleStopAndSend}
|
||||
aria-label="Stop and send"
|
||||
>
|
||||
<Square size={9} fill="currentColor" strokeWidth={0} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<TooltipSimple title="Voice input">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(): void => startVoiceInput(VoiceInputSource.Button)}
|
||||
onClick={(): void => startVoiceInput('button')}
|
||||
disabled={disabled}
|
||||
aria-label="Start voice input"
|
||||
className={styles.micBtn}
|
||||
prefix={<Mic size={14} />}
|
||||
/>
|
||||
>
|
||||
<Mic size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
))}
|
||||
|
||||
@@ -1183,21 +1026,21 @@ export default function ChatInput({
|
||||
color="destructive"
|
||||
onClick={onCancel}
|
||||
aria-label="Stop generating"
|
||||
prefix={<Square size={10} fill="currentColor" strokeWidth={0} />}
|
||||
/>
|
||||
>
|
||||
<Square size={10} fill="currentColor" strokeWidth={0} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
) : (
|
||||
<TooltipSimple title="Send message">
|
||||
<Button
|
||||
variant="solid"
|
||||
size="icon"
|
||||
color="primary"
|
||||
onClick={isListening ? handleStopAndSend : handleSend}
|
||||
disabled={disabled || (!text.trim() && pendingFiles.length === 0)}
|
||||
aria-label="Send message"
|
||||
prefix={<Send size={14} />}
|
||||
/>
|
||||
</TooltipSimple>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="icon"
|
||||
color="primary"
|
||||
onClick={isListening ? handleStopAndSend : handleSend}
|
||||
disabled={disabled || (!text.trim() && pendingFiles.length === 0)}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<Send size={14} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,19 +64,6 @@
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
// Mirrors `.field` for the multi_select group, but resets the browser's
|
||||
// default `<fieldset>` border/padding/margin so the visual matches the
|
||||
// `<div>`-based field rows.
|
||||
.multiSelectFieldset {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -63,14 +63,7 @@ export default function ClarificationForm({
|
||||
setAnswers((prev) => ({ ...prev, [id]: value }));
|
||||
};
|
||||
|
||||
const isFormValid = fields.every(
|
||||
(f) => !f.required || isFieldFilled(f, answers[f.id]),
|
||||
);
|
||||
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
if (!isFormValid) {
|
||||
return;
|
||||
}
|
||||
setSubmitted(true);
|
||||
// Approximate queryLength as the JSON encoding of the form answers — the
|
||||
// clarification API doesn't render a single user-visible string, but the
|
||||
@@ -143,7 +136,7 @@ export default function ClarificationForm({
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={isStreaming || !isFormValid}
|
||||
disabled={isStreaming}
|
||||
prefix={<Send />}
|
||||
>
|
||||
Submit
|
||||
@@ -169,9 +162,8 @@ export default function ClarificationForm({
|
||||
|
||||
/**
|
||||
* Per-type seed value. The DTO's `default` is `string | string[] | null`,
|
||||
* which doesn't fit boolean / number fields cleanly — we coerce 'true'/'false'
|
||||
* strings for booleans, parse number defaults out of the string form,
|
||||
* fall back to `[]` for multi_select, and the raw string otherwise.
|
||||
* which doesn't fit boolean fields cleanly — we coerce 'true'/'false' strings
|
||||
* for them, fall back to `[]` for multi_select, and the raw string otherwise.
|
||||
*/
|
||||
function initialAnswerFor(f: ClarificationFieldEventDTO): unknown {
|
||||
const raw = f.default;
|
||||
@@ -183,41 +175,9 @@ function initialAnswerFor(f: ClarificationFieldEventDTO): unknown {
|
||||
if (f.type === ClarificationFieldTypeDTO.multi_select) {
|
||||
return Array.isArray(raw) ? raw : [];
|
||||
}
|
||||
if (f.type === ClarificationFieldTypeDTO.number) {
|
||||
// Server sends number defaults as strings (e.g. `"5"`). Parse so the
|
||||
// stored value is a real `number` — otherwise `isFieldFilled` (which
|
||||
// requires `typeof === 'number'`) rejects a visibly-filled field and
|
||||
// Submit stays disabled.
|
||||
if (typeof raw !== 'string' || raw === '') {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(raw);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
return raw ?? '';
|
||||
}
|
||||
|
||||
// Whether a required field has been answered. Booleans are always considered
|
||||
// filled (they're initialised to a concrete true/false). For other types we
|
||||
// reject empty strings, empty arrays, NaN numbers, and `null` (which the
|
||||
// number input emits when its raw value is `''` — `Number('')` would
|
||||
// otherwise silently coerce to `0` and read as a valid answer).
|
||||
function isFieldFilled(
|
||||
field: ClarificationFieldEventDTO,
|
||||
value: unknown,
|
||||
): boolean {
|
||||
switch (field.type) {
|
||||
case ClarificationFieldTypeDTO.multi_select:
|
||||
return Array.isArray(value) && value.length > 0;
|
||||
case ClarificationFieldTypeDTO.boolean:
|
||||
return true;
|
||||
case ClarificationFieldTypeDTO.number:
|
||||
return typeof value === 'number' && !Number.isNaN(value);
|
||||
default:
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
interface FieldInputProps {
|
||||
field: ClarificationFieldEventDTO;
|
||||
value: unknown;
|
||||
@@ -256,21 +216,13 @@ function FieldInput({ field, value, onChange }: FieldInputProps): JSX.Element {
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor={id}>
|
||||
{label}
|
||||
{required && (
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
{required && <span className={styles.required}>*</span>}
|
||||
</label>
|
||||
<Select
|
||||
value={isCustom ? CUSTOM_OPTION_SENTINEL : String(value ?? '')}
|
||||
onChange={handleSelectChange}
|
||||
>
|
||||
<SelectTrigger
|
||||
id={id}
|
||||
placeholder="Select…"
|
||||
aria-required={required || undefined}
|
||||
/>
|
||||
<SelectTrigger id={id} placeholder="Select…" />
|
||||
{/* Pin the dropdown width to the trigger via Radix's
|
||||
`--radix-select-trigger-width`; otherwise the popover
|
||||
sizes to its widest item and looks misaligned. */}
|
||||
@@ -315,11 +267,7 @@ function FieldInput({ field, value, onChange }: FieldInputProps): JSX.Element {
|
||||
onChange={(): void => onChange(!checked)}
|
||||
>
|
||||
{label}
|
||||
{required && (
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
{required && <span className={styles.required}>*</span>}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
@@ -364,21 +312,11 @@ function FieldInput({ field, value, onChange }: FieldInputProps): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
// `fieldset` + `legend` is the WCAG-recommended grouping for
|
||||
// related checkboxes (1.3.1). SRs announce the legend before each
|
||||
// option, so users hear the group label as context.
|
||||
<fieldset
|
||||
className={styles.multiSelectFieldset}
|
||||
aria-required={required || undefined}
|
||||
>
|
||||
<legend className={styles.label}>
|
||||
<div className={styles.field}>
|
||||
<span className={styles.label}>
|
||||
{label}
|
||||
{required && (
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</legend>
|
||||
{required && <span className={styles.required}>*</span>}
|
||||
</span>
|
||||
<div className={styles.checkboxGroup}>
|
||||
{options?.map((opt) => (
|
||||
<Checkbox
|
||||
@@ -409,7 +347,7 @@ function FieldInput({ field, value, onChange }: FieldInputProps): JSX.Element {
|
||||
onChange={(e): void => updateCustomValue(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -418,29 +356,16 @@ function FieldInput({ field, value, onChange }: FieldInputProps): JSX.Element {
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor={id}>
|
||||
{label}
|
||||
{required && (
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
{required && <span className={styles.required}>*</span>}
|
||||
</label>
|
||||
<Input
|
||||
id={id}
|
||||
type={type === 'number' ? 'number' : 'text'}
|
||||
className={styles.input}
|
||||
value={String(value ?? '')}
|
||||
aria-required={required || undefined}
|
||||
onChange={(e): void => {
|
||||
if (type === 'number') {
|
||||
const raw = e.target.value;
|
||||
// Map empty input to `null` instead of `Number('') === 0`
|
||||
// so a required numeric field cleared after typing doesn't
|
||||
// silently read as a valid `0` in `isFieldFilled`.
|
||||
onChange(raw === '' ? null : Number(raw));
|
||||
} else {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
onChange={(e): void =>
|
||||
onChange(type === 'number' ? Number(e.target.value) : e.target.value)
|
||||
}
|
||||
placeholder={label}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@@ -9,10 +9,11 @@ import '../blocks';
|
||||
import { useVariant } from '../../VariantContext';
|
||||
import { Message, MessageBlock } from '../../types';
|
||||
import ActionsSection from '../ActionsSection';
|
||||
import ActivityGroup, { ActivityItem } from '../ActivityGroup';
|
||||
import { RichCodeBlock } from '../blocks';
|
||||
import { MessageContext } from '../MessageContext';
|
||||
import MessageFeedback from '../MessageFeedback';
|
||||
import ThinkingStep from '../ThinkingStep';
|
||||
import ToolCallStep from '../ToolCallStep';
|
||||
import UserMessageActions from '../UserMessageActions';
|
||||
|
||||
import styles from './MessageBubble.module.scss';
|
||||
@@ -39,61 +40,38 @@ function SmartPre({ children }: { children?: React.ReactNode }): JSX.Element {
|
||||
const MD_PLUGINS = [remarkGfm];
|
||||
const MD_COMPONENTS = { code: RichCodeBlock, pre: SmartPre };
|
||||
|
||||
type RenderGroup =
|
||||
| { kind: 'text'; id: string; content: string }
|
||||
| { kind: 'activity'; id: string; items: ActivityItem[] };
|
||||
|
||||
/**
|
||||
* Partition message blocks into render groups so consecutive thinking and
|
||||
* tool_call blocks collapse into a single ActivityGroup row. Text blocks
|
||||
* stand alone, mirroring the streaming view.
|
||||
*/
|
||||
function groupBlocks(blocks: MessageBlock[]): RenderGroup[] {
|
||||
const groups: RenderGroup[] = [];
|
||||
blocks.forEach((block, i) => {
|
||||
if (block.type === 'text') {
|
||||
groups.push({ kind: 'text', id: `text-${i}`, content: block.content });
|
||||
return;
|
||||
}
|
||||
const item: ActivityItem =
|
||||
block.type === 'thinking'
|
||||
? { id: `t-${i}`, kind: 'thinking', content: block.content }
|
||||
: {
|
||||
id: `c-${block.toolCallId}`,
|
||||
kind: 'tool',
|
||||
// Persisted blocks are always complete.
|
||||
toolCall: {
|
||||
toolName: block.toolName,
|
||||
input: block.toolInput,
|
||||
result: block.result,
|
||||
done: true,
|
||||
displayText: block.displayText,
|
||||
},
|
||||
};
|
||||
const last = groups[groups.length - 1];
|
||||
if (last?.kind === 'activity') {
|
||||
last.items.push(item);
|
||||
} else {
|
||||
groups.push({ kind: 'activity', id: `a-${i}`, items: [item] });
|
||||
}
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
function renderGroup(group: RenderGroup): JSX.Element {
|
||||
if (group.kind === 'text') {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={group.id}
|
||||
className={styles.markdown}
|
||||
remarkPlugins={MD_PLUGINS}
|
||||
components={MD_COMPONENTS}
|
||||
>
|
||||
{group.content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
/** Renders a single MessageBlock by type. */
|
||||
function renderBlock(block: MessageBlock, index: number): JSX.Element {
|
||||
switch (block.type) {
|
||||
case 'thinking':
|
||||
return <ThinkingStep key={index} content={block.content} />;
|
||||
case 'tool_call':
|
||||
// Blocks in a persisted message are always complete — done is always true.
|
||||
return (
|
||||
<ToolCallStep
|
||||
key={index}
|
||||
toolCall={{
|
||||
toolName: block.toolName,
|
||||
input: block.toolInput,
|
||||
result: block.result,
|
||||
done: true,
|
||||
displayText: block.displayText,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case 'text':
|
||||
default:
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={index}
|
||||
className={styles.markdown}
|
||||
remarkPlugins={MD_PLUGINS}
|
||||
components={MD_COMPONENTS}
|
||||
>
|
||||
{block.content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
return <ActivityGroup key={group.id} items={group.items} />;
|
||||
}
|
||||
|
||||
interface MessageBubbleProps {
|
||||
@@ -112,14 +90,6 @@ export default function MessageBubble({
|
||||
const isUser = message.role === 'user';
|
||||
const hasBlocks = !isUser && message.blocks && message.blocks.length > 0;
|
||||
|
||||
// Recompute groups only when the blocks array identity changes — store
|
||||
// updates that don't touch this message's blocks should not re-render the
|
||||
// underlying ThinkingStep/ToolCallStep children.
|
||||
const groups = useMemo(
|
||||
() => (hasBlocks ? groupBlocks(message.blocks!) : []),
|
||||
[hasBlocks, message.blocks],
|
||||
);
|
||||
|
||||
const messageClass = cx(
|
||||
styles.message,
|
||||
isUser ? styles.user : styles.assistant,
|
||||
@@ -158,7 +128,8 @@ export default function MessageBubble({
|
||||
<p className={styles.text}>{message.content}</p>
|
||||
) : hasBlocks ? (
|
||||
<MessageContext.Provider value={{ messageId: message.id }}>
|
||||
{groups.map((g) => renderGroup(g))}
|
||||
{/* eslint-disable-next-line react/no-array-index-key */}
|
||||
{message.blocks!.map((block, i) => renderBlock(block, i))}
|
||||
</MessageContext.Provider>
|
||||
) : (
|
||||
<MessageContext.Provider value={{ messageId: message.id }}>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { FeedbackRatingDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
|
||||
import { AIAssistantEvents } from '../../events';
|
||||
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
|
||||
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
|
||||
@@ -18,22 +17,6 @@ import { FeedbackRating, Message } from '../../types';
|
||||
|
||||
import styles from './MessageFeedback.module.scss';
|
||||
|
||||
const FEEDBACK_ANALYTICS_RATING = {
|
||||
[FeedbackRatingDTO.positive]: 'up',
|
||||
[FeedbackRatingDTO.negative]: 'down',
|
||||
} as const;
|
||||
|
||||
const VOTE_LABEL = {
|
||||
[FeedbackRatingDTO.positive]: {
|
||||
tooltip: 'Good response',
|
||||
ariaLabel: 'Good response',
|
||||
},
|
||||
[FeedbackRatingDTO.negative]: {
|
||||
tooltip: 'Bad response',
|
||||
ariaLabel: 'Bad response',
|
||||
},
|
||||
} as const;
|
||||
|
||||
interface MessageFeedbackProps {
|
||||
message: Message;
|
||||
onRegenerate?: () => void;
|
||||
@@ -134,7 +117,7 @@ export default function MessageFeedback({
|
||||
if (vote === rating) {
|
||||
return;
|
||||
}
|
||||
if (rating === FeedbackRatingDTO.negative) {
|
||||
if (rating === 'negative') {
|
||||
setNegativeComment('');
|
||||
setIsNegativeDialogOpen(true);
|
||||
return;
|
||||
@@ -143,7 +126,7 @@ export default function MessageFeedback({
|
||||
void logEvent(AIAssistantEvents.FeedbackSubmitted, {
|
||||
messageId: message.id,
|
||||
threadId,
|
||||
rating: FEEDBACK_ANALYTICS_RATING[rating],
|
||||
rating: 'up',
|
||||
hasComment: false,
|
||||
commentLength: 0,
|
||||
});
|
||||
@@ -153,21 +136,17 @@ export default function MessageFeedback({
|
||||
);
|
||||
|
||||
const handleSubmitNegative = useCallback((): void => {
|
||||
setVote(FeedbackRatingDTO.negative);
|
||||
setVote('negative');
|
||||
setIsNegativeDialogOpen(false);
|
||||
const trimmed = negativeComment.trim();
|
||||
void logEvent(AIAssistantEvents.FeedbackSubmitted, {
|
||||
messageId: message.id,
|
||||
threadId,
|
||||
rating: FEEDBACK_ANALYTICS_RATING[FeedbackRatingDTO.negative],
|
||||
rating: 'down',
|
||||
hasComment: trimmed.length > 0,
|
||||
commentLength: trimmed.length,
|
||||
});
|
||||
submitMessageFeedback(
|
||||
message.id,
|
||||
FeedbackRatingDTO.negative,
|
||||
trimmed || undefined,
|
||||
);
|
||||
submitMessageFeedback(message.id, 'negative', trimmed || undefined);
|
||||
}, [message.id, negativeComment, submitMessageFeedback, threadId]);
|
||||
|
||||
return (
|
||||
@@ -181,39 +160,32 @@ export default function MessageFeedback({
|
||||
variant="ghost"
|
||||
onClick={handleCopy}
|
||||
color="secondary"
|
||||
aria-label={copied ? 'Copied' : 'Copy message'}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title={VOTE_LABEL[FeedbackRatingDTO.positive].tooltip}>
|
||||
<TooltipSimple title="Good response">
|
||||
<Button
|
||||
className={cx(styles.btn, {
|
||||
[styles.votedUp]: vote === FeedbackRatingDTO.positive,
|
||||
})}
|
||||
className={cx(styles.btn, { [styles.votedUp]: vote === 'positive' })}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onClick={(): void => handleVote(FeedbackRatingDTO.positive)}
|
||||
aria-label={VOTE_LABEL[FeedbackRatingDTO.positive].ariaLabel}
|
||||
aria-pressed={vote === FeedbackRatingDTO.positive}
|
||||
onClick={(): void => handleVote('positive')}
|
||||
>
|
||||
<ThumbsUp size={12} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
|
||||
<TooltipSimple title={VOTE_LABEL[FeedbackRatingDTO.negative].tooltip}>
|
||||
<TooltipSimple title="Bad response">
|
||||
<Button
|
||||
className={cx(styles.btn, {
|
||||
[styles.votedDown]: vote === FeedbackRatingDTO.negative,
|
||||
[styles.votedDown]: vote === 'negative',
|
||||
})}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onClick={(): void => handleVote(FeedbackRatingDTO.negative)}
|
||||
aria-label={VOTE_LABEL[FeedbackRatingDTO.negative].ariaLabel}
|
||||
aria-pressed={vote === FeedbackRatingDTO.negative}
|
||||
onClick={(): void => handleVote('negative')}
|
||||
>
|
||||
<ThumbsDown size={12} />
|
||||
</Button>
|
||||
@@ -227,7 +199,6 @@ export default function MessageFeedback({
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onClick={onRegenerate}
|
||||
aria-label="Regenerate response"
|
||||
>
|
||||
<RefreshCw size={12} />
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@@ -10,10 +10,11 @@ import type {
|
||||
|
||||
import { useVariant } from '../../VariantContext';
|
||||
import { StreamingEventItem } from '../../types';
|
||||
import ActivityGroup, { ActivityItem } from '../ActivityGroup';
|
||||
import ApprovalCard from '../ApprovalCard';
|
||||
import { RichCodeBlock } from '../blocks';
|
||||
import ClarificationForm from '../ClarificationForm';
|
||||
import ThinkingStep from '../ThinkingStep';
|
||||
import ToolCallStep from '../ToolCallStep';
|
||||
|
||||
import messageStyles from '../MessageBubble/MessageBubble.module.scss';
|
||||
import styles from './StreamingMessage.module.scss';
|
||||
@@ -32,59 +33,6 @@ function SmartPre({ children }: { children?: React.ReactNode }): JSX.Element {
|
||||
const MD_PLUGINS = [remarkGfm];
|
||||
const MD_COMPONENTS = { code: RichCodeBlock, pre: SmartPre };
|
||||
|
||||
type RenderGroup =
|
||||
| { kind: 'text'; id: string; content: string }
|
||||
| {
|
||||
kind: 'activity';
|
||||
id: string;
|
||||
items: ActivityItem[];
|
||||
isTrailing: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Partition the streaming event timeline into render groups: runs of
|
||||
* consecutive thinking/tool events fold into a single activity group, text
|
||||
* events stay standalone. The last group is flagged as trailing so the
|
||||
* caller can drive a "live" indicator on it.
|
||||
*
|
||||
* Invariant relied on by the ActivityGroup elapsed-time timer: once a
|
||||
* group exists at a given array index, later events only extend its
|
||||
* `items` — they never shrink the array or re-key existing groups. That
|
||||
* keeps each ActivityGroup React instance stable across re-renders so the
|
||||
* timer's `wasLive` → `isLive` re-stamp captures the right transition.
|
||||
* The id fields below piggyback on that invariant: each event's position in
|
||||
* `events` is stable, so the derived id stays stable across re-renders.
|
||||
*/
|
||||
function groupStreamingEvents(events: StreamingEventItem[]): RenderGroup[] {
|
||||
const groups: RenderGroup[] = [];
|
||||
events.forEach((event, i) => {
|
||||
if (event.kind === 'text') {
|
||||
groups.push({ kind: 'text', id: `text-${i}`, content: event.content });
|
||||
return;
|
||||
}
|
||||
const item: ActivityItem =
|
||||
event.kind === 'thinking'
|
||||
? { id: `t-${i}`, kind: 'thinking', content: event.content }
|
||||
: { id: `c-${i}`, kind: 'tool', toolCall: event.toolCall };
|
||||
const last = groups[groups.length - 1];
|
||||
if (last?.kind === 'activity') {
|
||||
last.items.push(item);
|
||||
} else {
|
||||
groups.push({
|
||||
kind: 'activity',
|
||||
id: `a-${i}`,
|
||||
items: [item],
|
||||
isTrailing: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
const last = groups[groups.length - 1];
|
||||
if (last?.kind === 'activity') {
|
||||
last.isTrailing = true;
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** Human-readable labels for execution status codes shown before any events arrive. */
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
queued: 'Queued…',
|
||||
@@ -131,11 +79,6 @@ export default function StreamingMessage({
|
||||
[messageStyles.compact]: isCompact,
|
||||
});
|
||||
|
||||
// Recompute groups only when the events array identity changes. The
|
||||
// streaming reducer pushes new entries into the same array reference
|
||||
// once per tick, so this naturally invalidates as events arrive.
|
||||
const groups = useMemo(() => groupStreamingEvents(events), [events]);
|
||||
|
||||
return (
|
||||
<div className={messageClass}>
|
||||
<div className={messageStyles.bubble}>
|
||||
@@ -145,28 +88,27 @@ export default function StreamingMessage({
|
||||
)}
|
||||
{isEmpty && !statusLabel && <TypingDots />}
|
||||
|
||||
{/* Runs of consecutive thinking + tool events collapse into a
|
||||
single ActivityGroup; text events render inline between
|
||||
them. The trailing group is "live" while streaming is
|
||||
active and not blocked on the user. */}
|
||||
{groups.map((group) => {
|
||||
if (group.kind === 'text') {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
key={group.id}
|
||||
className={messageStyles.markdown}
|
||||
remarkPlugins={MD_PLUGINS}
|
||||
components={MD_COMPONENTS}
|
||||
>
|
||||
{group.content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
{/* eslint-disable react/no-array-index-key */}
|
||||
{/* Events rendered in arrival order: text, thinking, and tool calls interleaved */}
|
||||
{events.map((event, i) => {
|
||||
if (event.kind === 'tool') {
|
||||
return <ToolCallStep key={i} toolCall={event.toolCall} />;
|
||||
}
|
||||
if (event.kind === 'thinking') {
|
||||
return <ThinkingStep key={i} content={event.content} />;
|
||||
}
|
||||
const groupIsLive = group.isTrailing && !isWaitingOnUser;
|
||||
return (
|
||||
<ActivityGroup key={group.id} items={group.items} isLive={groupIsLive} />
|
||||
<ReactMarkdown
|
||||
key={i}
|
||||
className={messageStyles.markdown}
|
||||
remarkPlugins={MD_PLUGINS}
|
||||
components={MD_COMPONENTS}
|
||||
>
|
||||
{event.content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
})}
|
||||
{/* eslint-enable react/no-array-index-key */}
|
||||
|
||||
{/* While events are still streaming, append the typing dots so the
|
||||
user has a clear "more is coming" signal. Hidden when the agent
|
||||
|
||||
@@ -5,31 +5,11 @@ import styles from './ThinkingStep.module.scss';
|
||||
|
||||
interface ThinkingStepProps {
|
||||
content: string;
|
||||
/**
|
||||
* When false, label reads "Thought for a few seconds" — intentionally
|
||||
* vague because the API doesn't persist precise timing, so showing
|
||||
* seconds would be inconsistent between fresh and reloaded threads.
|
||||
*/
|
||||
isLive?: boolean;
|
||||
}
|
||||
|
||||
/** Body of a thinking step — extracted so ActivityGroup can render it directly. */
|
||||
export function ThinkingContent({ content }: { content: string }): JSX.Element {
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
<p className={styles.content}>{content}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function thinkingLabel(isLive: boolean): string {
|
||||
return isLive ? 'Thinking…' : 'Thought for a few seconds';
|
||||
}
|
||||
|
||||
/** Collapsible thinking row — chevron + label, content in the expanded body. */
|
||||
export default function ThinkingStep({
|
||||
content,
|
||||
isLive = false,
|
||||
}: ThinkingStepProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
@@ -43,10 +23,14 @@ export default function ThinkingStep({
|
||||
) : (
|
||||
<ChevronRight size={12} className={styles.chevron} />
|
||||
)}
|
||||
<span className={styles.label}>{thinkingLabel(isLive)}</span>
|
||||
<span className={styles.label}>Thinking</span>
|
||||
</div>
|
||||
|
||||
{expanded && <ThinkingContent content={content} />}
|
||||
{expanded && (
|
||||
<div className={styles.body}>
|
||||
<p className={styles.content}>{content}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,58 +10,24 @@ interface ToolCallStepProps {
|
||||
toolCall: StreamingToolCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-supplied `displayText` is the human-friendly title the backend
|
||||
* wants surfaced. Falls back to a derived label
|
||||
* ("signoz_get_dashboard" → "Get Dashboard") when missing.
|
||||
*/
|
||||
export function getToolDisplayLabel(toolCall: StreamingToolCall): string {
|
||||
const { toolName, displayText } = toolCall;
|
||||
if (displayText && displayText.trim().length > 0) {
|
||||
return displayText;
|
||||
}
|
||||
return toolName
|
||||
.replace(/^[a-z]+_/, '') // strip prefix like "signoz_"
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/** Body of a tool-call step — extracted so ActivityGroup can render it directly. */
|
||||
export function ToolCallContent({
|
||||
toolCall,
|
||||
}: {
|
||||
toolCall: StreamingToolCall;
|
||||
}): JSX.Element {
|
||||
const { toolName, input, result, done } = toolCall;
|
||||
return (
|
||||
<div className={styles.body}>
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Tool</span>
|
||||
<span className={styles.toolName}>{toolName}</span>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Input</span>
|
||||
<pre className={styles.json}>{JSON.stringify(input, null, 2)}</pre>
|
||||
</div>
|
||||
{done && result !== undefined && (
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Output</span>
|
||||
<pre className={styles.json}>
|
||||
{typeof result === 'string' ? result : JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Collapsible tool-call row — chevron + label, in/out detail in the body. */
|
||||
export default function ToolCallStep({
|
||||
toolCall,
|
||||
}: ToolCallStepProps): JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { done } = toolCall;
|
||||
const label = getToolDisplayLabel(toolCall);
|
||||
const { toolName, input, result, done, displayText } = toolCall;
|
||||
|
||||
// Prefer the server-supplied `displayText` from `ToolCallEventDTO` —
|
||||
// it's the human-friendly title the backend wants surfaced. Fall back
|
||||
// to a derived label ("signoz_get_dashboard" → "Get Dashboard") when
|
||||
// the field is empty / null / missing.
|
||||
const label =
|
||||
displayText && displayText.trim().length > 0
|
||||
? displayText
|
||||
: toolName
|
||||
.replace(/^[a-z]+_/, '') // strip prefix like "signoz_"
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
|
||||
const toggle = (): void => setExpanded((v) => !v);
|
||||
|
||||
@@ -78,7 +44,26 @@ export default function ToolCallStep({
|
||||
<span className={styles.label}>{label}</span>
|
||||
</div>
|
||||
|
||||
{expanded && <ToolCallContent toolCall={toolCall} />}
|
||||
{expanded && (
|
||||
<div className={styles.body}>
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Tool</span>
|
||||
<span className={styles.toolName}>{toolName}</span>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Input</span>
|
||||
<pre className={styles.json}>{JSON.stringify(input, null, 2)}</pre>
|
||||
</div>
|
||||
{done && result !== undefined && (
|
||||
<div className={styles.section}>
|
||||
<span className={styles.sectionLabel}>Output</span>
|
||||
<pre className={styles.json}>
|
||||
{typeof result === 'string' ? result : JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ export default function UserMessageActions({
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onClick={handleCopy}
|
||||
aria-label={copied ? 'Copied' : 'Copy message'}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
|
||||
@@ -90,16 +90,6 @@ export default function VirtualizedMessages({
|
||||
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
const scrollerRef = useRef<HTMLElement | Window | null>(null);
|
||||
// Tracks whether the scroller is pinned to (or near) the bottom. Updated
|
||||
// via Virtuoso's `atBottomStateChange` so we can stop force-scrolling the
|
||||
// user back down when they've intentionally scrolled up to read earlier
|
||||
// content.
|
||||
const atBottomRef = useRef(true);
|
||||
// Id of the latest user message we've already anchored to. Used to detect
|
||||
// a fresh user send so we can re-anchor to the bottom regardless of where
|
||||
// the user was scrolled — sending a message and not seeing it is worse
|
||||
// than the anti-yank guarantee.
|
||||
const lastSeenUserMessageIdRef = useRef<string | null>(null);
|
||||
|
||||
const handleRegenerate = useCallback(
|
||||
(messageId: string): void => {
|
||||
@@ -121,25 +111,8 @@ export default function VirtualizedMessages({
|
||||
// align: 'end')` would only reach the last item's bottom and leave the
|
||||
// padding hidden below the fold. Use `auto` while streaming so the bottom
|
||||
// stays glued as text deltas arrive; `smooth` lags when triggered every
|
||||
// few ms. Bail out if the user has scrolled away from the bottom — that's
|
||||
// an explicit signal they want to read earlier content without being
|
||||
// yanked back.
|
||||
// few ms.
|
||||
useEffect(() => {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const isFreshUserSend =
|
||||
lastMessage?.role === 'user' &&
|
||||
lastMessage.id !== lastSeenUserMessageIdRef.current;
|
||||
if (isFreshUserSend) {
|
||||
lastSeenUserMessageIdRef.current = lastMessage.id;
|
||||
// Re-anchor so the user sees their own send (and the assistant's
|
||||
// follow-up streaming) even if they were reading history when they
|
||||
// hit Enter.
|
||||
atBottomRef.current = true;
|
||||
}
|
||||
|
||||
if (!atBottomRef.current) {
|
||||
return;
|
||||
}
|
||||
const scroller = scrollerRef.current;
|
||||
if (!(scroller instanceof HTMLElement)) {
|
||||
return;
|
||||
@@ -149,7 +122,7 @@ export default function VirtualizedMessages({
|
||||
behavior: isStreaming ? 'auto' : 'smooth',
|
||||
});
|
||||
}, [
|
||||
messages,
|
||||
messages.length,
|
||||
streamingEvents.length,
|
||||
streamingContentLength,
|
||||
isStreaming,
|
||||
@@ -159,18 +132,14 @@ export default function VirtualizedMessages({
|
||||
|
||||
const followOutput = useCallback(
|
||||
(atBottom: boolean): false | 'auto' | 'smooth' => {
|
||||
if (!atBottom) {
|
||||
return false;
|
||||
if (isStreaming) {
|
||||
return 'auto';
|
||||
}
|
||||
return isStreaming ? 'auto' : 'smooth';
|
||||
return atBottom ? 'smooth' : false;
|
||||
},
|
||||
[isStreaming],
|
||||
);
|
||||
|
||||
const handleAtBottomStateChange = useCallback((atBottom: boolean): void => {
|
||||
atBottomRef.current = atBottom;
|
||||
}, []);
|
||||
|
||||
const showStreamingSlot =
|
||||
isStreaming || Boolean(pendingApproval) || Boolean(pendingClarification);
|
||||
|
||||
@@ -219,8 +188,6 @@ export default function VirtualizedMessages({
|
||||
className={styles.messages}
|
||||
totalCount={totalCount}
|
||||
followOutput={followOutput}
|
||||
atBottomStateChange={handleAtBottomStateChange}
|
||||
atBottomThreshold={64}
|
||||
initialTopMostItemIndex={Math.max(0, totalCount - 1)}
|
||||
itemContent={(index): JSX.Element => {
|
||||
if (index < messages.length) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
import SyntaxHighlighter, {
|
||||
a11yDark,
|
||||
@@ -127,17 +126,16 @@ function CopyButton({ text }: { text: string }): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipSimple title={copied ? 'Copied' : 'Copy code'}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
className={styles.copyBtn}
|
||||
onClick={handleCopy}
|
||||
aria-label={copied ? 'Copied' : 'Copy code'}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
className={styles.copyBtn}
|
||||
onClick={handleCopy}
|
||||
title={copied ? 'Copied' : 'Copy code'}
|
||||
aria-label={copied ? 'Copied' : 'Copy code'}
|
||||
>
|
||||
{copied ? <Check size={12} /> : <Copy size={12} />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,26 +63,6 @@ export const SuggestedPromptCategory = {
|
||||
export type SuggestedPromptCategory =
|
||||
(typeof SuggestedPromptCategory)[keyof typeof SuggestedPromptCategory];
|
||||
|
||||
// `source` attribute on the AI Assistant `Opened` event — describes which
|
||||
// surface triggered the open. Keep values stable: dashboards downstream
|
||||
// depend on the literal strings.
|
||||
export const AIAssistantOpenSource = {
|
||||
Icon: 'icon',
|
||||
Shortcut: 'shortcut',
|
||||
Cmdk: 'cmdk',
|
||||
} as const;
|
||||
export type AIAssistantOpenSource =
|
||||
(typeof AIAssistantOpenSource)[keyof typeof AIAssistantOpenSource];
|
||||
|
||||
// `source` attribute on the `VoiceInputUsed` event — which surface initiated
|
||||
// the recording.
|
||||
export const VoiceInputSource = {
|
||||
Button: 'button',
|
||||
Shortcut: 'shortcut',
|
||||
} as const;
|
||||
export type VoiceInputSource =
|
||||
(typeof VoiceInputSource)[keyof typeof VoiceInputSource];
|
||||
|
||||
export enum AIAssistantEvents {
|
||||
Opened = 'AI Assistant: Opened',
|
||||
MessageSent = 'AI Assistant: Message sent',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SIGNOZ_UPGRADE_PLAN_URL } from 'constants/app';
|
||||
import CreateAlertChannels from 'container/CreateAlertChannels';
|
||||
import { ChannelType } from 'container/CreateAlertChannels/config';
|
||||
import {
|
||||
@@ -312,6 +313,16 @@ describe('Create Alert Channel (Normal User)', () => {
|
||||
expect(screen.getByText('Microsoft Teams')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.skip('Should check if the upgrade plan message is shown', () => {
|
||||
expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/This feature is available for paid plans only./),
|
||||
).toBeInTheDocument();
|
||||
const link = screen.getByRole('link', { name: 'Click here' });
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute('href', SIGNOZ_UPGRADE_PLAN_URL);
|
||||
expect(screen.getByText(/to Upgrade/)).toBeInTheDocument();
|
||||
});
|
||||
it('Should check if the form buttons are displayed properly (Save, Test, Back)', () => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'button_save_channel' }),
|
||||
|
||||
@@ -17,7 +17,6 @@ import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQue
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useSyncTimeOnStagedQueryChange } from 'hooks/queryBuilder/useSyncTimeOnStagedQueryChange';
|
||||
import { useListOverview } from 'hooks/thirdPartyApis/useListOverview';
|
||||
import { get } from 'lodash-es';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -43,7 +42,7 @@ function DomainList(): JSX.Element {
|
||||
);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { currentQuery, handleRunQuery, stagedQuery } = useQueryBuilder();
|
||||
const { currentQuery, handleRunQuery } = useQueryBuilder();
|
||||
const query = useMemo(
|
||||
() => currentQuery?.builder?.queryData[0] || null,
|
||||
[currentQuery],
|
||||
@@ -70,8 +69,6 @@ function DomainList(): JSX.Element {
|
||||
handleRunQuery();
|
||||
}, [queryClient, handleRunQuery]);
|
||||
|
||||
useSyncTimeOnStagedQueryChange(stagedQuery?.id);
|
||||
|
||||
const { data, isLoading, isFetching } = useListOverview({
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
|
||||
@@ -1,32 +1,9 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
const PARAM_SEGMENT = /:[^/]+/g;
|
||||
const REGEX_SPECIALS = /[.+*?^$()[\]{}|\\]/g;
|
||||
|
||||
function templateToRegex(template: string): RegExp {
|
||||
const pattern = template
|
||||
.replace(REGEX_SPECIALS, '\\$&')
|
||||
.replace(PARAM_SEGMENT, '[^/]+');
|
||||
return new RegExp(`^${pattern}$`);
|
||||
}
|
||||
|
||||
export function getRouteKey(pathname: string): string {
|
||||
const entries = Object.entries(ROUTES);
|
||||
const [routeKey] = Object.entries(ROUTES).find(
|
||||
([, value]) => value === pathname,
|
||||
) || ['DEFAULT'];
|
||||
|
||||
const exact = entries.find(([, value]) => value === pathname);
|
||||
if (exact) {
|
||||
return exact[0];
|
||||
}
|
||||
|
||||
// First template that matches wins, so declaration order in `ROUTES`
|
||||
// matters when templates can overlap. Today's set is unambiguous because
|
||||
// `[^/]+` is segment-bounded, but if you ever add a sibling like
|
||||
// `/services/list` next to `SERVICE_METRICS: '/services/:servicename'`,
|
||||
// list the more-specific (more-static-segments) entry first in `ROUTES`
|
||||
// — otherwise the param template will swallow the static path.
|
||||
const dynamic = entries.find(
|
||||
([, value]) => value.includes(':') && templateToRegex(value).test(pathname),
|
||||
);
|
||||
|
||||
return dynamic?.[0] ?? 'DEFAULT';
|
||||
return routeKey;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Button, Input, Select, Tooltip } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Select, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { CircleX, Trash } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Collapse, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Collapse } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Input, Select } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import { ADVANCED_OPTIONS_TIME_UNIT_OPTIONS } from '../../context/constants';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Input } from 'antd';
|
||||
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import './TimeInput.scss';
|
||||
|
||||
export interface TimeInputProps {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Input, Select } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
Plus,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import { Button, Card, Input, Modal, Popover, Tag, Tooltip } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Card, Modal, Popover, Tag, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import {
|
||||
MetricRangePayloadProps,
|
||||
MetricRangePayloadV3,
|
||||
} from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { PanelMode } from '../../types';
|
||||
import { prepareBarPanelConfig } from '../utils';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
|
||||
() => ({
|
||||
getStoredSeriesVisibility: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('lib/uPlotLib/plugins/onClickPlugin', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockReturnValue({ name: 'onClickPlugin' }),
|
||||
}));
|
||||
|
||||
jest.mock('lib/dashboard/getQueryResults', () => ({
|
||||
getLegend: jest.fn(
|
||||
(_queryData: unknown, _query: unknown, labelName: string) =>
|
||||
`legend-${labelName}`,
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('lib/getLabelName', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(
|
||||
(_metric: unknown, _queryName: string, _legend: string) => 'baseLabel',
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils',
|
||||
() => ({
|
||||
getInitialStackedBands: jest.fn().mockReturnValue([]),
|
||||
}),
|
||||
);
|
||||
|
||||
const getLegendMock = jest.requireMock('lib/dashboard/getQueryResults')
|
||||
.getLegend as jest.Mock;
|
||||
const getLabelNameMock = jest.requireMock('lib/getLabelName')
|
||||
.default as jest.Mock;
|
||||
const getInitialStackedBandsMock = jest.requireMock(
|
||||
'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils',
|
||||
).getInitialStackedBands as jest.Mock;
|
||||
|
||||
const createApiResponse = (
|
||||
result: MetricRangePayloadProps['data']['result'] = [],
|
||||
): MetricRangePayloadProps => ({
|
||||
data: {
|
||||
result,
|
||||
resultType: 'matrix',
|
||||
newResult: null as unknown as MetricRangePayloadV3,
|
||||
},
|
||||
});
|
||||
|
||||
const createWidget = (overrides: Partial<Widgets> = {}): Widgets =>
|
||||
({
|
||||
id: 'widget-1',
|
||||
yAxisUnit: 'ms',
|
||||
isLogScale: false,
|
||||
thresholds: [],
|
||||
customLegendColors: {},
|
||||
...overrides,
|
||||
}) as Widgets;
|
||||
|
||||
const defaultTimezone = {
|
||||
name: 'UTC',
|
||||
value: 'UTC',
|
||||
offset: 'UTC',
|
||||
searchIndex: 'UTC',
|
||||
};
|
||||
|
||||
describe('BarPanel utils', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
getLabelNameMock.mockReturnValue('baseLabel');
|
||||
getLegendMock.mockImplementation(
|
||||
(_queryData: unknown, _query: unknown, labelName: string) =>
|
||||
`legend-${labelName}`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('prepareBarPanelData', () => {
|
||||
it('returns aligned data with timestamps and empty series when result is empty', () => {
|
||||
const data = prepareChartData(createApiResponse([]));
|
||||
expect(data).toHaveLength(1);
|
||||
expect(data[0]).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('returns timestamps and one series of y values for single series', () => {
|
||||
const data = prepareChartData(
|
||||
createApiResponse([
|
||||
{
|
||||
metric: {},
|
||||
queryName: 'Q',
|
||||
legend: 'Series A',
|
||||
values: [
|
||||
[1000, '10'],
|
||||
[2000, '20'],
|
||||
],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
]),
|
||||
);
|
||||
expect(data).toHaveLength(2);
|
||||
expect(data[0]).toStrictEqual([1000, 2000]);
|
||||
expect(data[1]).toStrictEqual([10, 20]);
|
||||
});
|
||||
|
||||
it('merges timestamps and fills missing values with null for multiple series', () => {
|
||||
const data = prepareChartData(
|
||||
createApiResponse([
|
||||
{
|
||||
metric: {},
|
||||
queryName: 'Q1',
|
||||
values: [
|
||||
[1000, '1'],
|
||||
[3000, '3'],
|
||||
],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
{
|
||||
metric: {},
|
||||
queryName: 'Q2',
|
||||
values: [
|
||||
[1000, '10'],
|
||||
[2000, '20'],
|
||||
],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
]),
|
||||
);
|
||||
expect(data[0]).toStrictEqual([1000, 2000, 3000]);
|
||||
expect(data[1]).toStrictEqual([1, null, 3]);
|
||||
expect(data[2]).toStrictEqual([10, 20, null]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareBarPanelConfig', () => {
|
||||
const baseParams = {
|
||||
widget: createWidget(),
|
||||
isDarkMode: true,
|
||||
currentQuery: {} as Query,
|
||||
onClick: jest.fn(),
|
||||
onDragSelect: jest.fn(),
|
||||
apiResponse: createApiResponse(),
|
||||
timezone: defaultTimezone,
|
||||
panelMode: PanelMode.DASHBOARD_VIEW,
|
||||
};
|
||||
|
||||
it('adds no series when apiResponse has empty result', () => {
|
||||
const config = prepareBarPanelConfig(baseParams).getConfig();
|
||||
expect(config.series).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('adds one series per result item', () => {
|
||||
const apiResponse = createApiResponse([
|
||||
{
|
||||
metric: {},
|
||||
queryName: 'Q1',
|
||||
values: [[1000, '1']],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
{
|
||||
metric: {},
|
||||
queryName: 'Q2',
|
||||
values: [[1000, '2']],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
]);
|
||||
const config = prepareBarPanelConfig({
|
||||
...baseParams,
|
||||
apiResponse,
|
||||
}).getConfig();
|
||||
expect(config.series).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('uses getLegend for label when currentQuery is provided', () => {
|
||||
const apiResponse = createApiResponse([
|
||||
{
|
||||
metric: {},
|
||||
queryName: 'Q1',
|
||||
legend: 'L1',
|
||||
values: [[1000, '1']],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
]);
|
||||
const config = prepareBarPanelConfig({
|
||||
...baseParams,
|
||||
apiResponse,
|
||||
currentQuery: {} as Query,
|
||||
}).getConfig();
|
||||
expect(getLegendMock).toHaveBeenCalled();
|
||||
expect(config.series?.[1]).toMatchObject({ label: 'legend-baseLabel' });
|
||||
});
|
||||
|
||||
it('uses getLabelName for label when currentQuery is null', () => {
|
||||
getLegendMock.mockReset();
|
||||
const apiResponse = createApiResponse([
|
||||
{
|
||||
metric: { __name__: 'requests' },
|
||||
queryName: 'Q1',
|
||||
values: [[1000, '1']],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
]);
|
||||
prepareBarPanelConfig({
|
||||
...baseParams,
|
||||
apiResponse,
|
||||
currentQuery: null as unknown as Query,
|
||||
});
|
||||
expect(getLabelNameMock).toHaveBeenCalled();
|
||||
expect(getLegendMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes result metric to each series for cross-panel sync', () => {
|
||||
const metric = { host: 'server1', __name__: 'http_requests' };
|
||||
const apiResponse = createApiResponse([
|
||||
{
|
||||
metric,
|
||||
queryName: 'Q1',
|
||||
values: [[1000, '1']],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
]);
|
||||
const config = prepareBarPanelConfig({
|
||||
...baseParams,
|
||||
apiResponse,
|
||||
}).getConfig();
|
||||
expect(config.series?.[1]).toMatchObject({ metric });
|
||||
});
|
||||
|
||||
it('uses widget customLegendColors for series stroke', () => {
|
||||
const widget = createWidget({
|
||||
customLegendColors: { 'legend-baseLabel': '#ff0000' },
|
||||
});
|
||||
const apiResponse = createApiResponse([
|
||||
{
|
||||
metric: {},
|
||||
queryName: 'Q',
|
||||
values: [[1000, '1']],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
]);
|
||||
const config = prepareBarPanelConfig({
|
||||
...baseParams,
|
||||
widget,
|
||||
apiResponse,
|
||||
}).getConfig();
|
||||
expect(config.series?.[1]).toMatchObject({ stroke: '#ff0000' });
|
||||
});
|
||||
|
||||
it('calls getInitialStackedBands when widget is stackedBarChart', () => {
|
||||
const widget = createWidget({ stackedBarChart: true });
|
||||
const apiResponse = createApiResponse([
|
||||
{
|
||||
metric: {},
|
||||
queryName: 'Q1',
|
||||
values: [[1000, '1']],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
{
|
||||
metric: {},
|
||||
queryName: 'Q2',
|
||||
values: [[1000, '2']],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
]);
|
||||
prepareBarPanelConfig({ ...baseParams, widget, apiResponse });
|
||||
// seriesCount = result.length + 1 = 3
|
||||
expect(getInitialStackedBandsMock).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
it('does not call getInitialStackedBands for non-stacked chart', () => {
|
||||
const apiResponse = createApiResponse([
|
||||
{
|
||||
metric: {},
|
||||
queryName: 'Q1',
|
||||
values: [[1000, '1']],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
]);
|
||||
prepareBarPanelConfig({ ...baseParams, apiResponse });
|
||||
expect(getInitialStackedBandsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -303,27 +303,6 @@ describe('TimeSeriesPanel utils', () => {
|
||||
expect(seriesConfig!.stroke).toBe('#ff0000');
|
||||
});
|
||||
|
||||
it('passes result metric to each series for cross-panel sync', () => {
|
||||
const metric = { host: 'server1', __name__: 'cpu' };
|
||||
const apiResponse = createApiResponse([
|
||||
{
|
||||
metric,
|
||||
queryName: 'Q',
|
||||
values: [
|
||||
[1000, '1'],
|
||||
[2000, '2'],
|
||||
],
|
||||
} as MetricRangePayloadProps['data']['result'][0],
|
||||
]);
|
||||
|
||||
const config = prepareUPlotConfig({
|
||||
...baseParams,
|
||||
apiResponse,
|
||||
}).getConfig();
|
||||
|
||||
expect(config.series?.[1]).toMatchObject({ metric });
|
||||
});
|
||||
|
||||
it('adds multiple series when result has multiple items', () => {
|
||||
const apiResponse = createApiResponse([
|
||||
{
|
||||
|
||||
@@ -19,11 +19,11 @@ import {
|
||||
Info,
|
||||
} from '@signozhq/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import {
|
||||
Button,
|
||||
ColorPicker,
|
||||
Divider,
|
||||
Input,
|
||||
Modal,
|
||||
RefSelectProps,
|
||||
Select,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input } from 'antd';
|
||||
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Form } from 'antd';
|
||||
import { EmailChannel } from '../../CreateAlertChannels/config';
|
||||
|
||||
function EmailForm({ setSelectedConfig }: EmailFormProps): JSX.Element {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Form } from 'antd';
|
||||
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
|
||||
|
||||
import { WebhookChannel } from '../../CreateAlertChannels/config';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Dispatch, ReactElement, SetStateAction } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, FormInstance, Input, Select } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Form, FormInstance, Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { Store } from 'antd/lib/form/interface';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
@@ -6,7 +6,8 @@ import { useIsFetching } from 'react-query';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Form, Input, Modal } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Form, Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -5,12 +5,12 @@ import { useCopyToClipboard } from 'react-use';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import {
|
||||
Col,
|
||||
Collapse,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Row,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Form, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Form } from 'antd';
|
||||
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
function RenderConnectionFields({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Form } from 'antd';
|
||||
import apply from 'api/v3/licenses/post';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { ChangeEvent, useState } from 'react';
|
||||
import { Button, Input, Modal } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Modal } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import ApacheIcon from 'assets/CustomIcons/ApacheIcon';
|
||||
import DockerIcon from 'assets/CustomIcons/DockerIcon';
|
||||
|
||||
@@ -12,11 +12,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Flex,
|
||||
Input,
|
||||
MenuProps,
|
||||
Modal,
|
||||
Popover,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoaderCircle, Check } from '@signozhq/icons';
|
||||
import { Button, Input, Space } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
|
||||
@@ -2,8 +2,9 @@ import { ReactNode, useState } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Collapse, Divider, Input, Tag } from 'antd';
|
||||
import { Collapse, Divider, Tag } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import getFromLocalstorage from 'api/browser/localstorage/get';
|
||||
import setToLocalstorage from 'api/browser/localstorage/set';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -37,12 +37,12 @@ import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSyncTimeOnStagedQueryChange } from 'hooks/queryBuilder/useSyncTimeOnStagedQueryChange';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
|
||||
import { isEmpty, isUndefined } from 'lodash-es';
|
||||
import LiveLogs from 'pages/LiveLogs';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
@@ -77,6 +77,7 @@ function LogsExplorerViewsContainer({
|
||||
handleChangeSelectedView: ChangeViewFunctionType;
|
||||
}): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [showFrequencyChart, setShowFrequencyChart] = useState(
|
||||
() => getFromLocalstorage(LOCALSTORAGE.SHOW_FREQUENCY_CHART) === 'true',
|
||||
@@ -89,17 +90,16 @@ function LogsExplorerViewsContainer({
|
||||
DEFAULT_PER_PAGE_VALUE,
|
||||
);
|
||||
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { minTime, maxTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const currentMinTimeRef = useRef<number>(minTime);
|
||||
|
||||
// Context
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
useSyncTimeOnStagedQueryChange(stagedQuery?.id);
|
||||
|
||||
const selectedPanelType = panelType || PANEL_TYPES.LIST;
|
||||
|
||||
// State
|
||||
@@ -329,6 +329,16 @@ function LogsExplorerViewsContainer({
|
||||
currentMinTimeRef.current !== minTime ||
|
||||
orderByChanged
|
||||
) {
|
||||
// Recalculate global time when query changes i.e. stage and run query clicked
|
||||
if (
|
||||
!!requestData?.id &&
|
||||
stagedQuery?.id &&
|
||||
requestData?.id !== stagedQuery?.id &&
|
||||
selectedTime !== 'custom'
|
||||
) {
|
||||
dispatch(UpdateTimeInterval(selectedTime));
|
||||
}
|
||||
|
||||
const newRequestData = getRequestData(stagedQuery, {
|
||||
filters: listQuery?.filters || initialFilters,
|
||||
filter: listQuery?.filter || { expression: '' },
|
||||
@@ -350,6 +360,8 @@ function LogsExplorerViewsContainer({
|
||||
minTime,
|
||||
activeLogId,
|
||||
selectedPanelType,
|
||||
dispatch,
|
||||
selectedTime,
|
||||
maxTime,
|
||||
orderBy,
|
||||
]);
|
||||
|
||||
@@ -2,7 +2,8 @@ import { ChangeEvent, useCallback, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CirclePlus, X } from '@signozhq/icons';
|
||||
import { Col, Input } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Col } from 'antd';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { SquareX, X } from '@signozhq/icons';
|
||||
import { Button, Input, Select } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Select } from 'antd';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import {
|
||||
ConditionalOperators,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Input } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Select } from 'antd';
|
||||
// TODO(@signozhq/ui-input): migrate this <Input> once @signozhq/ui Input
|
||||
// supports the `onWheel` handler (used to blur on scroll for number inputs).
|
||||
import { Input, Select } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { TIME_AGGREGATION_OPTIONS } from './constants';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import type { TableColumnsType as ColumnsType } from 'antd';
|
||||
import { Button, Collapse, Input, Select, Spin } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Collapse, Select, Spin } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
DropResult,
|
||||
} from 'react-beautiful-dnd';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Dropdown, Input, MenuProps, Tooltip } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Divider, Dropdown, MenuProps, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { FieldDataType } from 'api/v5/v5';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Col, Form, Input as AntInput, Input, Row } from 'antd';
|
||||
// TODO(@signozhq/ui-input): migrate <Input> once @signozhq/ui Input
|
||||
// supports the `spellCheck` prop on the URL input below.
|
||||
import { Button, Col, Form, Input, Input as AntInput, Row } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { CONTEXT_LINK_FIELDS } from 'container/NewWidget/RightContainer/ContextLinks/constants';
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Blocks, Check, LoaderCircle } from '@signozhq/icons';
|
||||
import { Button, Card, Form, Input, Select, Space } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Card, Form, Select, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Check, Server, LoaderCircle } from '@signozhq/icons';
|
||||
import { Button, Card, Form, Input, Space } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Card, Form, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
|
||||
@@ -295,6 +295,37 @@
|
||||
|
||||
.slider-container {
|
||||
width: calc(100% - 16px);
|
||||
|
||||
.ant-slider .ant-slider-mark {
|
||||
margin-top: 12px;
|
||||
|
||||
.ant-slider-mark-text {
|
||||
color: var(--l3-foreground);
|
||||
font-variant-numeric: lining-nums tabular-nums stacked-fractions
|
||||
slashed-zero;
|
||||
font-feature-settings:
|
||||
'dlig' on,
|
||||
'salt' on,
|
||||
'cpsp' on,
|
||||
'case' on;
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.logs-slider-container {
|
||||
.ant-slider .ant-slider-mark {
|
||||
.ant-slider-mark-text {
|
||||
&:last-child {
|
||||
left: calc(100% - 8px) !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.do-later-container {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Slider } from '@signozhq/ui/slider';
|
||||
import { Slider } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ArrowRight, LoaderCircle, Minus } from '@signozhq/icons';
|
||||
@@ -204,23 +204,23 @@ function OptimiseSignozNeeds({
|
||||
<label className="question-slider" htmlFor="organisationName">
|
||||
Logs / Day
|
||||
</label>
|
||||
<div className="slider-container">
|
||||
<div className="slider-container logs-slider-container">
|
||||
<div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
value={sliderValues.logsPerDay}
|
||||
marks={marks}
|
||||
onChange={(value): void =>
|
||||
handleSliderChange('logsPerDay', value as number)
|
||||
onChange={(value: number): void =>
|
||||
handleSliderChange('logsPerDay', value)
|
||||
}
|
||||
styles={{
|
||||
range: {
|
||||
backgroundColor: '#4E74F8',
|
||||
track: {
|
||||
background: '#4E74F8',
|
||||
},
|
||||
}}
|
||||
tooltip={{
|
||||
formatter: (): string => `${logsPerDayValue.toLocaleString()} GB`,
|
||||
formatter: (): string => `${logsPerDayValue.toLocaleString()} GB`, // Show whole number
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -238,16 +238,16 @@ function OptimiseSignozNeeds({
|
||||
max={100}
|
||||
value={sliderValues.hostsPerDay}
|
||||
marks={hostMarks}
|
||||
onChange={(value): void =>
|
||||
handleSliderChange('hostsPerDay', value as number)
|
||||
onChange={(value: number): void =>
|
||||
handleSliderChange('hostsPerDay', value)
|
||||
}
|
||||
styles={{
|
||||
range: {
|
||||
backgroundColor: '#4E74F8',
|
||||
track: {
|
||||
background: '#4E74F8',
|
||||
},
|
||||
}}
|
||||
tooltip={{
|
||||
formatter: (): string => `${hostsPerDayValue.toLocaleString()}`,
|
||||
formatter: (): string => `${hostsPerDayValue.toLocaleString()}`, // Show whole number
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -265,16 +265,16 @@ function OptimiseSignozNeeds({
|
||||
max={100}
|
||||
value={sliderValues.services}
|
||||
marks={serviceMarks}
|
||||
onChange={(value): void =>
|
||||
handleSliderChange('services', value as number)
|
||||
onChange={(value: number): void =>
|
||||
handleSliderChange('services', value)
|
||||
}
|
||||
styles={{
|
||||
range: {
|
||||
backgroundColor: '#4E74F8',
|
||||
track: {
|
||||
background: '#4E74F8',
|
||||
},
|
||||
}}
|
||||
tooltip={{
|
||||
formatter: (): string => `${servicesValue.toLocaleString()}`,
|
||||
formatter: (): string => `${servicesValue.toLocaleString()}`, // Show whole number
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus, Trash2 } from '@signozhq/icons';
|
||||
import { Button, Form, FormInstance, Input, Select, Space } from 'antd';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Form, FormInstance, Select, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input } from 'antd';
|
||||
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Form } from 'antd';
|
||||
import { ProcessorFormField } from '../../AddNewProcessor/config';
|
||||
import { formValidationRules } from '../../config';
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ChangeEventHandler, useState } from 'react';
|
||||
// TODO(@signozhq/ui-input): migrate to @signozhq/ui Input once the antd
|
||||
// `InputProps` spread (`size`, etc.) is no longer needed on this wrapper.
|
||||
import { Input, InputProps } from 'antd';
|
||||
|
||||
function CSVInput({ value, onChange, ...otherProps }: InputProps): JSX.Element {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Info } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Flex, Form, Input, Space, Tooltip } from 'antd';
|
||||
import { Flex, Form, Space, Tooltip } from 'antd';
|
||||
import { ProcessorData } from 'types/api/pipeline/def';
|
||||
|
||||
import { PREDEFINED_MAPPING } from '../config';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user