Compare commits

..

4 Commits

Author SHA1 Message Date
nityanandagohain
858e3699c9 fix: response model 2026-06-24 12:19:19 +05:30
nityanandagohain
ab71159312 fix: more updates 2026-06-24 11:25:05 +05:30
nityanandagohain
a207dd6bcf Merge remote-tracking branch 'origin/main' into issue_5325 2026-06-23 12:31:07 +05:30
nityanandagohain
1eb3ef40e1 feat: add endpoint for fetching unpriced models 2026-06-17 21:21:28 +05:30
400 changed files with 6440 additions and 20748 deletions

88
.github/CODEOWNERS vendored
View File

@@ -109,25 +109,6 @@ go.mod @therealpandey
/pkg/modules/role/ @therealpandey
/pkg/types/coretypes/ @therealpandey @vikrantgupta25
/frontend/src/hooks/useAuthZ/ @H4ad
/frontend/src/components/GuardAuthZ/ @H4ad
/frontend/src/components/AuthZTooltip/ @H4ad
/frontend/src/components/createGuardedRoute/ @H4ad
/frontend/src/container/RolesSettings/ @H4ad
/frontend/src/components/RolesSelect/ @H4ad
/frontend/src/pages/MembersSettings/ @H4ad
/frontend/src/container/MembersSettings/ @H4ad
/frontend/src/components/MembersTable/ @H4ad
/frontend/src/components/EditMemberDrawer/ @H4ad
/frontend/src/components/InviteMembersModal/ @H4ad
/frontend/src/hooks/member/ @H4ad
/frontend/src/pages/ServiceAccountsSettings/ @H4ad
/frontend/src/container/ServiceAccountsSettings/ @H4ad
/frontend/src/components/ServiceAccountsTable/ @H4ad
/frontend/src/components/ServiceAccountDrawer/ @H4ad
/frontend/src/components/CreateServiceAccountModal/ @H4ad
/frontend/src/hooks/serviceAccount/ @H4ad
# IdentN Owners
/pkg/identn/ @therealpandey
@@ -218,72 +199,3 @@ go.mod @therealpandey
## OpenAPI Schema - Generated
/frontend/src/api/generated/services/ @therealpandey @vikrantgupta25 @srikanthccv
/docs/api/openapi.yml @therealpandey @vikrantgupta25 @srikanthccv
## Logs
/frontend/src/pages/Logs/ @SigNoz/events-frontend
/frontend/src/pages/LogsExplorer/ @SigNoz/events-frontend
/frontend/src/pages/LogsModulePage/ @SigNoz/events-frontend
/frontend/src/pages/LogsSettings/ @SigNoz/events-frontend
/frontend/src/pages/LiveLogs/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerChart/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerContext/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerList/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerTable/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerViews/ @SigNoz/events-frontend
/frontend/src/container/LogsFilters/ @SigNoz/events-frontend
/frontend/src/container/LogsSearchFilter/ @SigNoz/events-frontend
/frontend/src/container/LogsTable/ @SigNoz/events-frontend
/frontend/src/container/LogsAggregate/ @SigNoz/events-frontend
/frontend/src/container/LogsContextList/ @SigNoz/events-frontend
/frontend/src/container/LogsIndexToFields/ @SigNoz/events-frontend
/frontend/src/container/LogsLoading/ @SigNoz/events-frontend
/frontend/src/container/LogsPanelTable/ @SigNoz/events-frontend
/frontend/src/container/LogControls/ @SigNoz/events-frontend
/frontend/src/container/LogDetailedView/ @SigNoz/events-frontend
/frontend/src/container/LogExplorerQuerySection/ @SigNoz/events-frontend
/frontend/src/container/LogLiveTail/ @SigNoz/events-frontend
/frontend/src/container/LiveLogs/ @SigNoz/events-frontend
/frontend/src/container/EmptyLogsSearch/ @SigNoz/events-frontend
/frontend/src/container/NoLogs/ @SigNoz/events-frontend
/frontend/src/components/Logs/ @SigNoz/events-frontend
/frontend/src/components/LogDetail/ @SigNoz/events-frontend
/frontend/src/components/LogsFormatOptionsMenu/ @SigNoz/events-frontend
/frontend/src/hooks/logs/ @SigNoz/events-frontend
## Logs Pipelines
/frontend/src/pages/Pipelines/ @SigNoz/events-frontend
/frontend/src/container/PipelinePage/ @SigNoz/events-frontend
## Traces / Trace Explorer
/frontend/src/pages/Trace/ @SigNoz/events-frontend
/frontend/src/pages/TracesExplorer/ @SigNoz/events-frontend
/frontend/src/pages/TracesModulePage/ @SigNoz/events-frontend
/frontend/src/container/Trace/ @SigNoz/events-frontend
/frontend/src/container/TracesExplorer/ @SigNoz/events-frontend
/frontend/src/container/TracesTableComponent/ @SigNoz/events-frontend
## Trace Funnels
/frontend/src/pages/TracesFunnels/ @SigNoz/events-frontend
/frontend/src/pages/TracesFunnelDetails/ @SigNoz/events-frontend
/frontend/src/hooks/TracesFunnels/ @SigNoz/events-frontend
## Trace Details
/frontend/src/pages/TraceDetailsV3/ @SigNoz/events-frontend
/frontend/src/pages/TraceDetailOldRedirect/ @SigNoz/events-frontend
/frontend/src/hooks/trace/ @SigNoz/events-frontend
## Exceptions
/frontend/src/pages/AllErrors/ @SigNoz/events-frontend
/frontend/src/pages/ErrorDetails/ @SigNoz/events-frontend
/frontend/src/container/AllError/ @SigNoz/events-frontend
/frontend/src/container/ErrorDetails/ @SigNoz/events-frontend
## External APIs
/frontend/src/pages/ApiMonitoring/ @SigNoz/events-frontend
/frontend/src/container/ApiMonitoring/ @SigNoz/events-frontend
## Messaging Queues
/frontend/src/pages/MessagingQueues/ @SigNoz/events-frontend
/frontend/src/components/MessagingQueues/ @SigNoz/events-frontend
/frontend/src/components/MessagingQueueHealthCheck/ @SigNoz/events-frontend
/frontend/src/hooks/messagingQueue/ @SigNoz/events-frontend

View File

@@ -140,20 +140,3 @@ jobs:
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)
transaction-groups:
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-transaction-groups
run: |
go run cmd/enterprise/*.go generate config transaction-groups
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in transaction groups schema. Run go run cmd/enterprise/*.go generate config transaction-groups locally and commit."; exit 1)

View File

@@ -29,8 +29,6 @@ import (
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule/implmetricreductionrule"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
@@ -121,9 +119,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(_ sqlstore.SQLStore, _ dashboard.Module, _ global.Global, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module, _ cloudintegration.Config) (cloudintegration.Module, error) {
return implcloudintegration.NewModule(), nil
},
func() metricreductionrule.Module {
return implmetricreductionrule.NewModule()
},
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
return factory.MustNewNamedMap(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, nil, nil))
},

View File

@@ -24,7 +24,6 @@ import (
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration/implcloudprovider"
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
eeimplmetricreductionrule "github.com/SigNoz/signoz/ee/modules/metricreductionrule/implmetricreductionrule"
eequerier "github.com/SigNoz/signoz/ee/querier"
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
eerules "github.com/SigNoz/signoz/ee/query-service/rules"
@@ -47,7 +46,6 @@ import (
pkgcloudintegration "github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
@@ -184,9 +182,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return implcloudintegration.NewModule(pkgcloudintegration.NewStore(sqlStore), dashboardModule, global, zeus, gateway, licensing, serviceAccount, cloudProvidersMap, config)
},
func() metricreductionrule.Module {
return eeimplmetricreductionrule.NewModule()
},
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
return factory.MustNewNamedMap(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, eerules.PrepareTaskFunc, eerules.TestNotification))
},

View File

@@ -6,15 +6,12 @@ import (
"reflect"
"strings"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/web"
"github.com/spf13/cobra"
"github.com/swaggest/jsonschema-go"
)
const webSettingsSchemaPath = "frontend/src/schemas/generated/webSettings.schema.json"
const transactionGroupsSchemaPath = "frontend/src/schemas/generated/transactionGroups.schema.json"
const webSettingsSchemaPath = "docs/config/web-settings.json"
func registerGenerateConfig(parentCmd *cobra.Command) {
configCmd := &cobra.Command{
@@ -30,14 +27,6 @@ func registerGenerateConfig(parentCmd *cobra.Command) {
},
})
configCmd.AddCommand(&cobra.Command{
Use: "transaction-groups",
Short: "Generate JSON Schema for transaction groups",
RunE: func(currCmd *cobra.Command, args []string) error {
return generateTransactionGroups()
},
})
parentCmd.AddCommand(configCmd)
}
@@ -63,7 +52,6 @@ func generateWebSettings() error {
return err
}
schema.WithTitle("WebSettings")
data, err := json.MarshalIndent(schema, "", " ")
if err != nil {
return err
@@ -71,31 +59,3 @@ func generateWebSettings() error {
return os.WriteFile(webSettingsSchemaPath, append(data, '\n'), 0o600)
}
func generateTransactionGroups() 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
}),
)
schema, err := reflector.Reflect(authtypes.TransactionGroups{})
if err != nil {
return err
}
schema.WithTitle("TransactionGroups")
data, err := json.MarshalIndent(schema, "", " ")
if err != nil {
return err
}
return os.WriteFile(transactionGroupsSchemaPath, append(data, '\n'), 0o600)
}

View File

@@ -1,129 +0,0 @@
# Migrating from the install script to Foundry
> [!IMPORTANT]
> The install script is now deprecated and will no longer receive updates.
This guide walks you through migrating an existing SigNoz deployment running via
Docker Compose to [Foundry](https://signoz.io/docs/install/docker/).
> [!NOTE]
> Setting up SigNoz for the first time? You don't need this guide — follow the [SigNoz installation docs](https://signoz.io/docs/install/) instead.
## Overview
To stay up to date on new installation platforms and patterns, please refer to [Foundry](https://github.com/SigNoz/foundry).
Two `foundryctl` commands are used throughout this guide:
- **`forge`** — generates deployment manifests from your `casting.yaml`. It does not touch running containers, so it is safe to re-run while you iterate.
- **`cast`** — applies the generated manifests: it creates and starts the containers (and pulls new images).
## Prerequisites
- [ ] Install Foundry - `curl -fsSL https://signoz.io/foundry.sh | bash`
## Migration Steps
> [!WARNING]
> **Before proceeding, back up both:**
> - **Your docker volumes** — these hold your data.
> - **Your existing `docker-compose.yaml` (and any config it references)** — keep a copy somewhere safe. The compose manifests are no longer distributed by SigNoz, so this backup is your only way to roll back to your previous setup.
1. Make a note of the volume names used by your existing deployment for the following components:
- ClickHouse
- SigNoz
- ZooKeeper
> If you used the docker compose file we provided, the volumes will be `signoz-clickhouse`, `signoz-sqlite`, and `signoz-zookeeper-1`.
2. Generate your `casting.yaml`. Based on internal testing, the following casting should generate the manifests that mimic the legacy docker compose setup (compare against your backed-up `docker-compose.yaml`). Once created, run `foundryctl forge -f casting.yaml`.
Foundry has a [Docker Compose example](https://github.com/SigNoz/foundry/tree/main/docs/examples/docker/compose). Please use it as a reference.
> [!WARNING]
> If your deployment had more than 1 shard or replica, you will need to adjust your manifest volumes accordingly.
> [!IMPORTANT]
> The `replica` and `shard` macros below are placeholders. Replace them with the values from your existing ClickHouse configuration (check the `macros` section of your current ClickHouse config, e.g. `config.xml`/`metrika.xml`), otherwise the generated manifests will not match your existing data.
```yaml
apiVersion: v1alpha1
kind: Installation
metadata:
name: signoz
spec:
deployment:
flavor: compose
mode: docker
metastore:
kind: sqlite
telemetrykeeper:
kind: zookeeper
telemetrystore:
spec:
config:
data:
config-0-0.yaml: |
macros:
replica: "example01-01-1" # replace with your existing ClickHouse replica macro (see legacy configuration files for reference)
shard: "01" # replace with your existing ClickHouse shard macro (see legacy configuration files for reference)
patches:
- target: "deployment/compose.yaml"
operations:
- op: replace
path: /volumes/signoz-telemetrykeeper-0-data/name
value: signoz-zookeeper-1
- op: replace
path: /volumes/signoz-telemetrystore-0-0-data/name
value: signoz-clickhouse
- op: replace
path: /volumes/signoz-metastore-sqlite-0-data/name
value: signoz-sqlite
- op: add
path: /services/signoz-telemetrykeeper-zookeeper-0/user
value: root
```
> [!NOTE]
> The `user: root` patch on the ZooKeeper service is required so the container can read/write the data in your reused ZooKeeper volume, which was created with `root`-owned files by the legacy compose setup. Without it, ZooKeeper may fail to start with permission errors.
If you had custom configurations for features like SMTP or additional ingestion processors/receivers, you will need to include those in your casting file via [patches](https://github.com/SigNoz/foundry/blob/main/docs/concepts/patches.md), [custom configuration](https://github.com/SigNoz/foundry/blob/main/docs/concepts/moldings.md#custom-config-files) or [environment variables](https://github.com/SigNoz/foundry/blob/main/docs/reference/casting-file.md#molding-spec) based on your previous configuration.
3. Review your manifests, we suggest executing the following checks on your manifests before proceeding:
- [ ] Validate the container images match what your deployment had, Foundry uses `latest` on generation by default.
- [ ] If your signoz version was older than latest, please check the [upgrade path](https://signoz.io/docs/operate/upgrade/) first.
- [ ] Check the produced manifests in `pours/deployment` match your older configurations. Extra consideration and review needs to be done on `compose.yaml` as this will be the main entry point for your new deployment.
- [ ] The configuration files for clickhouse are now in YAML so validate your custom settings are present.
4. Execute a `docker compose down`. **Do not** include parameters such as `--volumes` (or `-v`), as it will wipe the volumes we need to maintain and reuse to avoid data loss.
> [!NOTE]
> This will generate downtime so please plan accordingly.
5. Validate the SigNoz containers are down with `docker ps -a`. Multiple containers cannot bind the same volume.
6. Run `foundryctl cast -f casting.yaml`. This will recreate the containers based on the spec. This process will download new container images.
> [!NOTE]
> When `cast` is run, the migration container will execute its migrations.
## Verifying the Migration
- SigNoz containers will be up and running.
- Log in to the SigNoz UI and verify that data is present.
- Signoz will run on localhost:8080
- Validate that your data ingestion is receiving data.
- Ingesters will receive data on localhost:4317(grpc) and localhost:4318(http)
- Review the logs from both ClickHouse and ZooKeeper; no errors should be present.
## Rolling Back
Because step 4 brought the legacy stack down *without* `-v`, your original volumes
are untouched and still hold your data. To roll back:
- Stop and remove the containers created by Foundry (`docker compose down`, again without `-v`).
- Confirm the containers are gone with `docker ps -a` so nothing else is bound to the volumes.
- Reapply your original docker compose file (`docker compose up -d`). It will reattach to the
existing volumes and restore your prior state.
## Troubleshooting
- Please reach out to our community on [Slack](https://signoz.io/slack).
## References
- [SigNoz Docker installation docs](https://signoz.io/docs/install/docker/)
- [SigNoz documentation](https://signoz.io/docs)
- [Foundry](https://github.com/SigNoz/foundry)

View File

@@ -3,16 +3,77 @@
Check that you have cloned [signoz/signoz](https://github.com/signoz/signoz)
and currently are in `signoz/deploy` folder.
## Installation
## Docker
> **Note:** The `install.sh` script and the `docker-compose` manifests have been deprecated.
If you don't have docker set up, please follow [this guide](https://docs.docker.com/engine/install/)
to set up docker before proceeding with the next steps.
SigNoz now installs and runs through [Foundry](https://signoz.io/docs/install/docker/).
### Using Install Script
> **Already running SigNoz via Docker Compose?** See the [Migration Guide](./MIGRATION.md) to transition your existing deployment to Foundry.
Now run the following command to install:
Please follow the latest installation instructions at [signoz.io/docs/install/docker](https://signoz.io/docs/install/docker/).
Foundry has support for **different platforms and architectures**, please review the project documentation for more details.
```sh
./install.sh
```
### Using Docker Compose
If you don't have docker compose set up, please follow [this guide](https://docs.docker.com/compose/install/)
to set up docker compose before proceeding with the next steps.
```sh
cd deploy/docker
docker compose up -d
```
Open http://localhost:8080 in your favourite browser.
To start collecting logs and metrics from your infrastructure, run the following command:
```sh
cd generator/infra
docker compose up -d
```
To start generating sample traces, run the following command:
```sh
cd generator/hotrod
docker compose up -d
```
In a couple of minutes, you should see the data generated from hotrod in SigNoz UI.
For more details, please refer to the [SigNoz documentation](https://signoz.io/docs/install/docker/).
## Docker Swarm
To install SigNoz using Docker Swarm, run the following command:
```sh
cd deploy/docker-swarm
docker stack deploy -c docker-compose.yaml signoz
```
Open http://localhost:8080 in your favourite browser.
To start collecting logs and metrics from your infrastructure, run the following command:
```sh
cd generator/infra
docker stack deploy -c docker-compose.yaml infra
```
To start generating sample traces, run the following command:
```sh
cd generator/hotrod
docker stack deploy -c docker-compose.yaml hotrod
```
In a couple of minutes, you should see the data generated from hotrod in SigNoz UI.
For more details, please refer to the [SigNoz documentation](https://signoz.io/docs/install/docker-swarm/).
## Uninstall/Troubleshoot?

View File

@@ -0,0 +1,75 @@
<?xml version="1.0"?>
<clickhouse>
<!-- ZooKeeper is used to store metadata about replicas, when using Replicated tables.
Optional. If you don't use replicated tables, you could omit that.
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/
-->
<zookeeper>
<node index="1">
<host>zookeeper-1</host>
<port>2181</port>
</node>
<node index="2">
<host>zookeeper-2</host>
<port>2181</port>
</node>
<node index="3">
<host>zookeeper-3</host>
<port>2181</port>
</node>
</zookeeper>
<!-- Configuration of clusters that could be used in Distributed tables.
https://clickhouse.com/docs/en/operations/table_engines/distributed/
-->
<remote_servers>
<cluster>
<!-- Inter-server per-cluster secret for Distributed queries
default: no secret (no authentication will be performed)
If set, then Distributed queries will be validated on shards, so at least:
- such cluster should exist on the shard,
- such cluster should have the same secret.
And also (and which is more important), the initial_user will
be used as current user for the query.
Right now the protocol is pretty simple and it only takes into account:
- cluster name
- query
Also it will be nice if the following will be implemented:
- source hostname (see interserver_http_host), but then it will depends from DNS,
it can use IP address instead, but then the you need to get correct on the initiator node.
- target hostname / ip address (same notes as for source hostname)
- time-based security tokens
-->
<!-- <secret></secret> -->
<shard>
<!-- Optional. Whether to write data to just one of the replicas. Default: false (write data to all replicas). -->
<!-- <internal_replication>false</internal_replication> -->
<!-- Optional. Shard weight when writing data. Default: 1. -->
<!-- <weight>1</weight> -->
<replica>
<host>clickhouse</host>
<port>9000</port>
<!-- Optional. Priority of the replica for load_balancing. Default: 1 (less value has more priority). -->
<!-- <priority>1</priority> -->
</replica>
</shard>
<shard>
<replica>
<host>clickhouse-2</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>clickhouse-3</host>
<port>9000</port>
</replica>
</shard>
</cluster>
</remote_servers>
</clickhouse>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0"?>
<clickhouse>
<!-- ZooKeeper is used to store metadata about replicas, when using Replicated tables.
Optional. If you don't use replicated tables, you could omit that.
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/
-->
<zookeeper>
<node index="1">
<host>zookeeper-1</host>
<port>2181</port>
</node>
<!-- <node index="2">
<host>zookeeper-2</host>
<port>2181</port>
</node>
<node index="3">
<host>zookeeper-3</host>
<port>2181</port>
</node> -->
</zookeeper>
<!-- Configuration of clusters that could be used in Distributed tables.
https://clickhouse.com/docs/en/operations/table_engines/distributed/
-->
<remote_servers>
<cluster>
<!-- Inter-server per-cluster secret for Distributed queries
default: no secret (no authentication will be performed)
If set, then Distributed queries will be validated on shards, so at least:
- such cluster should exist on the shard,
- such cluster should have the same secret.
And also (and which is more important), the initial_user will
be used as current user for the query.
Right now the protocol is pretty simple and it only takes into account:
- cluster name
- query
Also it will be nice if the following will be implemented:
- source hostname (see interserver_http_host), but then it will depends from DNS,
it can use IP address instead, but then the you need to get correct on the initiator node.
- target hostname / ip address (same notes as for source hostname)
- time-based security tokens
-->
<!-- <secret></secret> -->
<shard>
<!-- Optional. Whether to write data to just one of the replicas. Default: false (write data to all replicas). -->
<!-- <internal_replication>false</internal_replication> -->
<!-- Optional. Shard weight when writing data. Default: 1. -->
<!-- <weight>1</weight> -->
<replica>
<host>clickhouse</host>
<port>9000</port>
<!-- Optional. Priority of the replica for load_balancing. Default: 1 (less value has more priority). -->
<!-- <priority>1</priority> -->
</replica>
</shard>
<!-- <shard>
<replica>
<host>clickhouse-2</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>clickhouse-3</host>
<port>9000</port>
</replica>
</shard> -->
</cluster>
</remote_servers>
</clickhouse>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
<functions>
<function>
<type>executable</type>
<name>histogramQuantile</name>
<return_type>Float64</return_type>
<argument>
<type>Array(Float64)</type>
<name>buckets</name>
</argument>
<argument>
<type>Array(Float64)</type>
<name>counts</name>
</argument>
<argument>
<type>Float64</type>
<name>quantile</name>
</argument>
<format>CSV</format>
<command>./histogramQuantile</command>
</function>
</functions>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0"?>
<clickhouse>
<storage_configuration>
<disks>
<default>
<keep_free_space_bytes>10485760</keep_free_space_bytes>
</default>
<s3>
<type>s3</type>
<!-- For S3 cold storage,
if region is us-east-1, endpoint can be https://<bucket-name>.s3.amazonaws.com
if region is not us-east-1, endpoint should be https://<bucket-name>.s3-<region>.amazonaws.com
For GCS cold storage,
endpoint should be https://storage.googleapis.com/<bucket-name>/data/
-->
<endpoint>https://BUCKET-NAME.s3-REGION-NAME.amazonaws.com/data/</endpoint>
<access_key_id>ACCESS-KEY-ID</access_key_id>
<secret_access_key>SECRET-ACCESS-KEY</secret_access_key>
<!-- In case of S3, uncomment the below configuration in case you want to read
AWS credentials from the Environment variables if they exist. -->
<!-- <use_environment_credentials>true</use_environment_credentials> -->
<!-- In case of GCS, uncomment the below configuration, since GCS does
not support batch deletion and result in error messages in logs. -->
<!-- <support_batch_delete>false</support_batch_delete> -->
</s3>
</disks>
<policies>
<tiered>
<volumes>
<default>
<disk>default</disk>
</default>
<s3>
<disk>s3</disk>
<perform_ttl_move_on_insert>0</perform_ttl_move_on_insert>
</s3>
</volumes>
</tiered>
</policies>
</storage_configuration>
</clickhouse>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0"?>
<clickhouse>
<!-- See also the files in users.d directory where the settings can be overridden. -->
<!-- Profiles of settings. -->
<profiles>
<!-- Default settings. -->
<default>
<!-- Maximum memory usage for processing single query, in bytes. -->
<max_memory_usage>10000000000</max_memory_usage>
<!-- How to choose between replicas during distributed query processing.
random - choose random replica from set of replicas with minimum number of errors
nearest_hostname - from set of replicas with minimum number of errors, choose replica
with minimum number of different symbols between replica's hostname and local hostname
(Hamming distance).
in_order - first live replica is chosen in specified order.
first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors.
-->
<load_balancing>random</load_balancing>
</default>
<!-- Profile that allows only read queries. -->
<readonly>
<readonly>1</readonly>
</readonly>
</profiles>
<!-- Users and ACL. -->
<users>
<!-- If user name was not specified, 'default' user is used. -->
<default>
<!-- See also the files in users.d directory where the password can be overridden.
Password could be specified in plaintext or in SHA256 (in hex format).
If you want to specify password in plaintext (not recommended), place it in 'password' element.
Example: <password>qwerty</password>.
Password could be empty.
If you want to specify SHA256, place it in 'password_sha256_hex' element.
Example: <password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>
Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019).
If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,
place its name in 'server' element inside 'ldap' element.
Example: <ldap><server>my_ldap_server</server></ldap>
If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config),
place 'kerberos' element instead of 'password' (and similar) elements.
The name part of the canonical principal name of the initiator must match the user name for authentication to succeed.
You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests
whose initiator's realm matches it.
Example: <kerberos />
Example: <kerberos><realm>EXAMPLE.COM</realm></kerberos>
How to generate decent password:
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-'
In first line will be password and in second - corresponding SHA256.
How to generate double SHA1:
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-'
In first line will be password and in second - corresponding double SHA1.
-->
<password></password>
<!-- List of networks with open access.
To open access from everywhere, specify:
<ip>::/0</ip>
To open access only from localhost, specify:
<ip>::1</ip>
<ip>127.0.0.1</ip>
Each element of list has one of the following forms:
<ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0
2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.
<host> Hostname. Example: server01.clickhouse.com.
To check access, DNS query is performed, and all received addresses compared to peer address.
<host_regexp> Regular expression for host names. Example, ^server\d\d-\d\d-\d\.clickhouse\.com$
To check access, DNS PTR query is performed for peer address and then regexp is applied.
Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.
Strongly recommended that regexp is ends with $
All results of DNS requests are cached till server restart.
-->
<networks>
<ip>::/0</ip>
</networks>
<!-- Settings profile for user. -->
<profile>default</profile>
<!-- Quota for user. -->
<quota>default</quota>
<!-- User can create other users and grant rights to them. -->
<!-- <access_management>1</access_management> -->
</default>
</users>
<!-- Quotas. -->
<quotas>
<!-- Name of quota. -->
<default>
<!-- Limits for time interval. You could specify many intervals with different limits. -->
<interval>
<!-- Length of interval. -->
<duration>3600</duration>
<!-- No limits. Just calculate resource usage for time interval. -->
<queries>0</queries>
<errors>0</errors>
<result_rows>0</result_rows>
<read_rows>0</read_rows>
<execution_time>0</execution_time>
</interval>
</default>
</quotas>
</clickhouse>

View File

View File

@@ -0,0 +1,16 @@
from locust import HttpUser, task, between
class UserTasks(HttpUser):
wait_time = between(5, 15)
@task
def rachel(self):
self.client.get("/dispatch?customer=123&nonse=0.6308392664170006")
@task
def trom(self):
self.client.get("/dispatch?customer=392&nonse=0.015296363321630757")
@task
def japanese(self):
self.client.get("/dispatch?customer=731&nonse=0.8022286220408668")
@task
def coffee(self):
self.client.get("/dispatch?customer=567&nonse=0.0022220379420636593")

View File

@@ -0,0 +1 @@
server_endpoint: ws://signoz:4320/v1/opamp

View File

@@ -0,0 +1,25 @@
# my global config
global:
scrape_interval: 5s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files: []
# - "first_rules.yml"
# - "second_rules.yml"
# - 'alerts.yml'
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs: []
remote_read:
- url: tcp://clickhouse:9000/signoz_metrics

View File

@@ -0,0 +1,288 @@
version: "3"
x-common: &common
networks:
- signoz-net
deploy:
restart_policy:
condition: on-failure
logging:
options:
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
tty: true
deploy:
labels:
signoz.io/scrape: "true"
signoz.io/port: "9363"
signoz.io/path: "/metrics"
depends_on:
- zookeeper-1
- zookeeper-2
- zookeeper-3
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- 0.0.0.0:8123/ping
interval: 30s
timeout: 5s
retries: 3
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
user: root
deploy:
labels:
signoz.io/scrape: "true"
signoz.io/port: "9141"
signoz.io/path: "/metrics"
healthcheck:
test:
- CMD-SHELL
- curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null
interval: 30s
timeout: 5s
retries: 3
x-db-depend: &db-depend
!!merge <<: *common
depends_on:
- clickhouse
- clickhouse-2
- clickhouse-3
services:
init-clickhouse:
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
command:
- bash
- -c
- |
version="v0.0.1"
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
cd /tmp
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
tar -xvzf histogram-quantile.tar.gz
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
deploy:
restart_policy:
condition: on-failure
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
!!merge <<: *zookeeper-defaults
# ports:
# - "2181:2181"
# - "2888:2888"
# - "3888:3888"
volumes:
- ./clickhouse-setup/data/zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
zookeeper-2:
!!merge <<: *zookeeper-defaults
# ports:
# - "2182:2181"
# - "2889:2888"
# - "3889:3888"
volumes:
- ./clickhouse-setup/data/zookeeper-2:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=2
- ZOO_SERVERS=zookeeper-1:2888:3888,0.0.0.0:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
zookeeper-3:
!!merge <<: *zookeeper-defaults
# ports:
# - "2183:2181"
# - "2890:2888"
# - "3890:3888"
volumes:
- ./clickhouse-setup/data/zookeeper-3:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=3
- ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
!!merge <<: *clickhouse-defaults
# TODO: needed for schema-migrator to work, remove this redundancy once we have a better solution
hostname: clickhouse
# ports:
# - "9000:9000"
# - "8123:8123"
# - "9181:9181"
configs:
- source: clickhouse-config
target: /etc/clickhouse-server/config.xml
- source: clickhouse-users
target: /etc/clickhouse-server/users.xml
- source: clickhouse-custom-function
target: /etc/clickhouse-server/custom-function.xml
- source: clickhouse-cluster
target: /etc/clickhouse-server/config.d/cluster.ha.xml
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ./clickhouse-setup/data/clickhouse/:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
clickhouse-2:
!!merge <<: *clickhouse-defaults
hostname: clickhouse-2
# ports:
# - "9001:9000"
# - "8124:8123"
# - "9182:9181"
configs:
- source: clickhouse-config
target: /etc/clickhouse-server/config.xml
- source: clickhouse-users
target: /etc/clickhouse-server/users.xml
- source: clickhouse-custom-function
target: /etc/clickhouse-server/custom-function.xml
- source: clickhouse-cluster
target: /etc/clickhouse-server/config.d/cluster.ha.xml
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ./clickhouse-setup/data/clickhouse-2/:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
clickhouse-3:
!!merge <<: *clickhouse-defaults
hostname: clickhouse-3
# ports:
# - "9002:9000"
# - "8125:8123"
# - "9183:9181"
configs:
- source: clickhouse-config
target: /etc/clickhouse-server/config.xml
- source: clickhouse-users
target: /etc/clickhouse-server/users.xml
- source: clickhouse-custom-function
target: /etc/clickhouse-server/custom-function.xml
- source: clickhouse-cluster
target: /etc/clickhouse-server/config.d/cluster.ha.xml
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ./clickhouse-setup/data/clickhouse-3/:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.129.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port
volumes:
- ./clickhouse-setup/data/signoz/:/var/lib/signoz/
environment:
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:8080/api/v1/health
interval: 30s
timeout: 5s
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.5
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate sync check &&
/signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml
configs:
- source: otel-collector-config
target: /etc/otel-collector-config.yaml
- source: otel-manager-config
target: /etc/manager-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
- LOW_CARDINAL_EXCEPTION_GROUPING=false
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
deploy:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.5
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate bootstrap &&
/signoz-otel-collector migrate sync up &&
/signoz-otel-collector migrate async up
networks:
signoz-net:
name: signoz-net
volumes:
clickhouse:
name: signoz-clickhouse
clickhouse-2:
name: signoz-clickhouse-2
clickhouse-3:
name: signoz-clickhouse-3
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1
zookeeper-2:
name: signoz-zookeeper-2
zookeeper-3:
name: signoz-zookeeper-3
configs:
clickhouse-config:
file: ../common/clickhouse/config.xml
clickhouse-users:
file: ../common/clickhouse/users.xml
clickhouse-custom-function:
file: ../common/clickhouse/custom-function.xml
clickhouse-cluster:
file: ../common/clickhouse/cluster.ha.xml
otel-collector-config:
file: ./otel-collector-config.yaml
otel-manager-config:
file: ../common/signoz/otel-collector-opamp-config.yaml

View File

@@ -0,0 +1,206 @@
version: "3"
x-common: &common
networks:
- signoz-net
deploy:
restart_policy:
condition: on-failure
logging:
options:
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
tty: true
deploy:
labels:
signoz.io/scrape: "true"
signoz.io/port: "9363"
signoz.io/path: "/metrics"
depends_on:
- init-clickhouse
- zookeeper-1
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- 0.0.0.0:8123/ping
interval: 30s
timeout: 5s
retries: 3
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
user: root
deploy:
labels:
signoz.io/scrape: "true"
signoz.io/port: "9141"
signoz.io/path: "/metrics"
healthcheck:
test:
- CMD-SHELL
- curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null
interval: 30s
timeout: 5s
retries: 3
x-db-depend: &db-depend
!!merge <<: *common
depends_on:
- clickhouse
services:
init-clickhouse:
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
command:
- bash
- -c
- |
version="v0.0.1"
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
cd /tmp
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
tar -xvzf histogram-quantile.tar.gz
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
deploy:
restart_policy:
condition: on-failure
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
!!merge <<: *zookeeper-defaults
# ports:
# - "2181:2181"
# - "2888:2888"
# - "3888:3888"
volumes:
- zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
!!merge <<: *clickhouse-defaults
# TODO: needed for clickhouse TCP connectio
hostname: clickhouse
# ports:
# - "9000:9000"
# - "8123:8123"
# - "9181:9181"
configs:
- source: clickhouse-config
target: /etc/clickhouse-server/config.xml
- source: clickhouse-users
target: /etc/clickhouse-server/users.xml
- source: clickhouse-custom-function
target: /etc/clickhouse-server/custom-function.xml
- source: clickhouse-cluster
target: /etc/clickhouse-server/config.d/cluster.xml
volumes:
- clickhouse:/var/lib/clickhouse/
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.129.0
ports:
- "8080:8080" # signoz port
volumes:
- sqlite:/var/lib/signoz/
environment:
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:8080/api/v1/health
interval: 30s
timeout: 5s
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.5
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate sync check &&
/signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml
configs:
- source: otel-collector-config
target: /etc/otel-collector-config.yaml
- source: otel-manager-config
target: /etc/manager-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
- LOW_CARDINAL_EXCEPTION_GROUPING=false
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
deploy:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.5
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate bootstrap &&
/signoz-otel-collector migrate sync up &&
/signoz-otel-collector migrate async up
networks:
signoz-net:
name: signoz-net
volumes:
clickhouse:
name: signoz-clickhouse
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1
configs:
clickhouse-config:
file: ../common/clickhouse/config.xml
clickhouse-users:
file: ../common/clickhouse/users.xml
clickhouse-custom-function:
file: ../common/clickhouse/custom-function.xml
clickhouse-cluster:
file: ../common/clickhouse/cluster.xml
otel-collector-config:
file: ./otel-collector-config.yaml
otel-manager-config:
file: ../common/signoz/otel-collector-opamp-config.yaml

View File

@@ -0,0 +1,118 @@
connectors:
signozmeter:
metrics_flush_interval: 1h
dimensions:
- name: service.name
- name: deployment.environment
- name: host.name
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector
processors:
batch:
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
batch/meter:
send_batch_max_size: 25000
send_batch_size: 20000
timeout: 1s
resourcedetection:
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
detectors: [env, system]
timeout: 2s
signozspanmetrics/delta:
metrics_exporter: signozclickhousemetrics
metrics_flush_interval: 60s
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
dimensions_cache_size: 100000
aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
enable_exp_histogram: true
dimensions:
- name: service.namespace
default: default
- name: deployment.environment
default: default
# This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics
- name: signoz.collector.id
- name: service.version
- name: browser.platform
- name: browser.mobile
- name: k8s.cluster.name
- name: k8s.node.name
- name: k8s.namespace.name
- name: host.name
- name: host.type
- name: container.name
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/signoz_traces
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
use_new_schema: true
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
timeout: 10s
use_new_schema: true
signozclickhousemeter:
dsn: tcp://clickhouse:9000/signoz_meter
timeout: 45s
sending_queue:
enabled: false
metadataexporter:
cache:
provider: in_memory
dsn: tcp://clickhouse:9000/signoz_metadata
enabled: true
timeout: 45s
service:
telemetry:
logs:
encoding: json
extensions:
- health_check
- pprof
pipelines:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces, metadataexporter, signozmeter]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
metrics/meter:
receivers: [signozmeter]
processors: [batch/meter]
exporters: [signozclickhousemeter]

1
deploy/docker/.env Normal file
View File

@@ -0,0 +1 @@
COMPOSE_PROJECT_NAME=signoz

View File

@@ -0,0 +1,3 @@
This data directory is deprecated and will be removed in the future.
Please use the migration script under `scripts/volume-migration` to migrate data from bind mounts to Docker volumes.
The script also renames the project name to `signoz` and the network name to `signoz-net` (if not already in place).

View File

View File

@@ -0,0 +1,2 @@
This directory is deprecated and will be removed in the future.
Please use the new directory for Clickhouse setup scripts: `scripts/clickhouse` instead.

View File

@@ -0,0 +1,265 @@
version: "3"
x-common: &common
networks:
- signoz-net
restart: unless-stopped
logging:
options:
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
image: clickhouse/clickhouse-server:25.5.6
tty: true
labels:
signoz.io/scrape: "true"
signoz.io/port: "9363"
signoz.io/path: "/metrics"
depends_on:
init-clickhouse:
condition: service_completed_successfully
zookeeper-1:
condition: service_healthy
zookeeper-2:
condition: service_healthy
zookeeper-3:
condition: service_healthy
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- 0.0.0.0:8123/ping
interval: 30s
timeout: 5s
retries: 3
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
signoz.io/port: "9141"
signoz.io/path: "/metrics"
healthcheck:
test:
- CMD-SHELL
- curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null
interval: 30s
timeout: 5s
retries: 3
x-db-depend: &db-depend
!!merge <<: *common
depends_on:
clickhouse:
condition: service_healthy
clickhouse-2:
condition: service_healthy
clickhouse-3:
condition: service_healthy
services:
init-clickhouse:
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
container_name: signoz-init-clickhouse
command:
- bash
- -c
- |
version="v0.0.1"
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
cd /tmp
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
tar -xvzf histogram-quantile.tar.gz
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
restart: on-failure
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-1
# ports:
# - "2181:2181"
# - "2888:2888"
# - "3888:3888"
volumes:
- zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
zookeeper-2:
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-2
# ports:
# - "2182:2181"
# - "2889:2888"
# - "3889:3888"
volumes:
- zookeeper-2:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=2
- ZOO_SERVERS=zookeeper-1:2888:3888,0.0.0.0:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
zookeeper-3:
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-3
# ports:
# - "2183:2181"
# - "2890:2888"
# - "3890:3888"
volumes:
- zookeeper-3:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=3
- ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse
# ports:
# - "9000:9000"
# - "8123:8123"
# - "9181:9181"
volumes:
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.ha.xml:/etc/clickhouse-server/config.d/cluster.xml
- clickhouse:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
clickhouse-2:
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse-2
# ports:
# - "9001:9000"
# - "8124:8123"
# - "9182:9181"
volumes:
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.ha.xml:/etc/clickhouse-server/config.d/cluster.xml
- clickhouse-2:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
clickhouse-3:
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse-3
# ports:
# - "9002:9000"
# - "8125:8123"
# - "9183:9181"
volumes:
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.ha.xml:/etc/clickhouse-server/config.d/cluster.xml
- clickhouse-3:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.129.0}
container_name: signoz
ports:
- "8080:8080" # signoz port
volumes:
- sqlite:/var/lib/signoz/
environment:
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:8080/api/v1/health
interval: 30s
timeout: 5s
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate sync check &&
/signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- LOW_CARDINAL_EXCEPTION_GROUPING=false
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate bootstrap &&
/signoz-otel-collector migrate sync up &&
/signoz-otel-collector migrate async up
restart: on-failure
networks:
signoz-net:
name: signoz-net
volumes:
clickhouse:
name: signoz-clickhouse
clickhouse-2:
name: signoz-clickhouse-2
clickhouse-3:
name: signoz-clickhouse-3
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1
zookeeper-2:
name: signoz-zookeeper-2
zookeeper-3:
name: signoz-zookeeper-3

View File

@@ -0,0 +1,185 @@
version: "3"
x-common: &common
networks:
- signoz-net
restart: unless-stopped
logging:
options:
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
tty: true
labels:
signoz.io/scrape: "true"
signoz.io/port: "9363"
signoz.io/path: "/metrics"
depends_on:
init-clickhouse:
condition: service_completed_successfully
zookeeper-1:
condition: service_healthy
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- 0.0.0.0:8123/ping
interval: 30s
timeout: 5s
retries: 3
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
signoz.io/port: "9141"
signoz.io/path: "/metrics"
healthcheck:
test:
- CMD-SHELL
- curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null
interval: 30s
timeout: 5s
retries: 3
x-db-depend: &db-depend
!!merge <<: *common
depends_on:
clickhouse:
condition: service_healthy
services:
init-clickhouse:
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
container_name: signoz-init-clickhouse
command:
- bash
- -c
- |
version="v0.0.1"
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
cd /tmp
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
tar -xvzf histogram-quantile.tar.gz
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
restart: on-failure
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-1
# ports:
# - "2181:2181"
# - "2888:2888"
# - "3888:3888"
volumes:
- zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse
# ports:
# - "9000:9000"
# - "8123:8123"
# - "9181:9181"
volumes:
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
- clickhouse:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.129.0}
container_name: signoz
ports:
- "8080:8080" # signoz port
volumes:
- sqlite:/var/lib/signoz/
environment:
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:8080/api/v1/health
interval: 30s
timeout: 5s
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate sync check &&
/signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- LOW_CARDINAL_EXCEPTION_GROUPING=false
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate bootstrap &&
/signoz-otel-collector migrate sync up &&
/signoz-otel-collector migrate async up
restart: on-failure
networks:
signoz-net:
name: signoz-net
volumes:
clickhouse:
name: signoz-clickhouse
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1

View File

@@ -0,0 +1,118 @@
connectors:
signozmeter:
metrics_flush_interval: 1h
dimensions:
- name: service.name
- name: deployment.environment
- name: host.name
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector
processors:
batch:
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
batch/meter:
send_batch_max_size: 25000
send_batch_size: 20000
timeout: 1s
resourcedetection:
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
detectors: [env, system]
timeout: 2s
signozspanmetrics/delta:
metrics_exporter: signozclickhousemetrics
metrics_flush_interval: 60s
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
dimensions_cache_size: 100000
aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
enable_exp_histogram: true
dimensions:
- name: service.namespace
default: default
- name: deployment.environment
default: default
# This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics
- name: signoz.collector.id
- name: service.version
- name: browser.platform
- name: browser.mobile
- name: k8s.cluster.name
- name: k8s.node.name
- name: k8s.namespace.name
- name: host.name
- name: host.type
- name: container.name
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/signoz_traces
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
use_new_schema: true
signozclickhousemetrics:
dsn: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/signoz_logs
timeout: 10s
use_new_schema: true
signozclickhousemeter:
dsn: tcp://clickhouse:9000/signoz_meter
timeout: 45s
sending_queue:
enabled: false
metadataexporter:
cache:
provider: in_memory
dsn: tcp://clickhouse:9000/signoz_metadata
enabled: true
timeout: 45s
service:
telemetry:
logs:
encoding: json
extensions:
- health_check
- pprof
pipelines:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces, metadataexporter, signozmeter]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics, metadataexporter, signozmeter]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter, metadataexporter, signozmeter]
metrics/meter:
receivers: [signozmeter]
processors: [batch/meter]
exporters: [signozclickhousemeter]

View File

@@ -2,24 +2,562 @@
set -o errexit
# Variables
BASE_DIR="$(dirname "$(readlink -f "$0")")"
DOCKER_STANDALONE_DIR="docker"
DOCKER_SWARM_DIR="docker-swarm" # TODO: Add docker swarm support
# Regular Colors
Yellow='\033[0;33m' # Yellow
Green='\033[0;32m' # Green
NC='\033[0m' # No Color
Black='\033[0;30m' # Black
Red='\[\e[0;31m\]' # Red
Green='\033[0;32m' # Green
Yellow='\033[0;33m' # Yellow
Blue='\033[0;34m' # Blue
Purple='\033[0;35m' # Purple
Cyan='\033[0;36m' # Cyan
White='\033[0;37m' # White
NC='\033[0m' # No Color
is_command_present() {
type "$1" >/dev/null 2>&1
}
# Check whether 'wget' command exists.
has_wget() {
has_cmd wget
}
# Check whether 'curl' command exists.
has_curl() {
has_cmd curl
}
# Check whether the given command exists.
has_cmd() {
command -v "$1" > /dev/null 2>&1
}
# Check if docker compose plugin is present
has_docker_compose_plugin() {
docker compose version > /dev/null 2>&1
}
is_mac() {
[[ $OSTYPE == darwin* ]]
}
is_arm64(){
[[ `uname -m` == 'arm64' || `uname -m` == 'aarch64' ]]
}
check_os() {
if is_mac; then
package_manager="brew"
desired_os=1
os="Mac"
return
fi
if is_arm64; then
arch="arm64"
arch_official="aarch64"
else
arch="amd64"
arch_official="x86_64"
fi
platform=$(uname -s | tr '[:upper:]' '[:lower:]')
os_name="$(cat /etc/*-release | awk -F= '$1 == "NAME" { gsub(/"/, ""); print $2; exit }')"
case "$os_name" in
Ubuntu*|Pop!_OS)
desired_os=1
os="ubuntu"
package_manager="apt-get"
;;
Amazon\ Linux*)
desired_os=1
os="amazon linux"
package_manager="yum"
;;
Debian*)
desired_os=1
os="debian"
package_manager="apt-get"
;;
Linux\ Mint*)
desired_os=1
os="linux mint"
package_manager="apt-get"
;;
Red\ Hat*)
desired_os=1
os="rhel"
package_manager="yum"
;;
CentOS*)
desired_os=1
os="centos"
package_manager="yum"
;;
Rocky*)
desired_os=1
os="centos"
package_manager="yum"
;;
SLES*)
desired_os=1
os="sles"
package_manager="zypper"
;;
openSUSE*)
desired_os=1
os="opensuse"
package_manager="zypper"
;;
*)
desired_os=0
os="Not Found: $os_name"
esac
}
# This function checks if the relevant ports required by SigNoz are available or not
# The script should error out in case they aren't available
check_ports_occupied() {
local port_check_output
local ports_pattern="8080|4317"
if is_mac; then
port_check_output="$(netstat -anp tcp | awk '$6 == "LISTEN" && $4 ~ /^.*\.('"$ports_pattern"')$/')"
elif is_command_present ss; then
# The `ss` command seems to be a better/faster version of `netstat`, but is not available on all Linux
# distributions by default. Other distributions have `ss` but no `netstat`. So, we try for `ss` first, then
# fallback to `netstat`.
port_check_output="$(ss --all --numeric --tcp | awk '$1 == "LISTEN" && $4 ~ /^.*:('"$ports_pattern"')$/')"
elif is_command_present netstat; then
port_check_output="$(netstat --all --numeric --tcp | awk '$6 == "LISTEN" && $4 ~ /^.*:('"$ports_pattern"')$/')"
fi
if [[ -n $port_check_output ]]; then
send_event "port_not_available"
echo "+++++++++++ ERROR ++++++++++++++++++++++"
echo "SigNoz requires ports 8080 & 4317 to be open. Please shut down any other service(s) that may be running on these ports."
echo "You can run SigNoz on another port following this guide https://signoz.io/docs/install/troubleshooting/"
echo "++++++++++++++++++++++++++++++++++++++++"
echo ""
exit 1
fi
}
install_docker() {
echo "++++++++++++++++++++++++"
echo "Setting up docker repos"
if [[ $package_manager == apt-get ]]; then
apt_cmd="$sudo_cmd apt-get --yes --quiet"
$apt_cmd update
$apt_cmd install software-properties-common gnupg-agent
curl -fsSL "https://download.docker.com/linux/$os/gpg" | $sudo_cmd apt-key add -
$sudo_cmd add-apt-repository \
"deb [arch=$arch] https://download.docker.com/linux/$os $(lsb_release -cs) stable"
$apt_cmd update
echo "Installing docker"
$apt_cmd install docker-ce docker-ce-cli containerd.io
elif [[ $package_manager == zypper ]]; then
zypper_cmd="$sudo_cmd zypper --quiet --no-gpg-checks --non-interactive"
echo "Installing docker"
if [[ $os == sles ]]; then
os_sp="$(cat /etc/*-release | awk -F= '$1 == "VERSION_ID" { gsub(/"/, ""); print $2; exit }')"
os_arch="$(uname -i)"
SUSEConnect -p sle-module-containers/$os_sp/$os_arch -r ''
fi
$zypper_cmd install docker docker-runc containerd
$sudo_cmd systemctl enable docker.service
elif [[ $package_manager == yum && $os == 'amazon linux' ]]; then
echo
echo "Amazon Linux detected ... "
echo
# yum install docker
# service docker start
$sudo_cmd yum install -y amazon-linux-extras
$sudo_cmd amazon-linux-extras enable docker
$sudo_cmd yum install -y docker
else
yum_cmd="$sudo_cmd yum --assumeyes --quiet"
$yum_cmd install yum-utils
$sudo_cmd yum-config-manager --add-repo https://download.docker.com/linux/$os/docker-ce.repo
echo "Installing docker"
$yum_cmd install docker-ce docker-ce-cli containerd.io
fi
}
compose_version () {
local compose_version
compose_version="$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d\" -f4)"
echo "${compose_version:-v2.18.1}"
}
install_docker_compose() {
if [[ $package_manager == "apt-get" || $package_manager == "zypper" || $package_manager == "yum" ]]; then
if [[ ! -f /usr/bin/docker-compose ]];then
echo "++++++++++++++++++++++++"
echo "Installing docker-compose"
compose_url="https://github.com/docker/compose/releases/download/$(compose_version)/docker-compose-$platform-$arch_official"
echo "Downloading docker-compose from $compose_url"
$sudo_cmd curl -L "$compose_url" -o /usr/local/bin/docker-compose
$sudo_cmd chmod +x /usr/local/bin/docker-compose
$sudo_cmd ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
echo "docker-compose installed!"
echo ""
fi
else
send_event "docker_compose_not_found"
echo "+++++++++++ IMPORTANT READ ++++++++++++++++++++++"
echo "docker-compose not found! Please install docker-compose first and then continue with this installation."
echo "Refer https://docs.docker.com/compose/install/ for installing docker-compose."
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
exit 1
fi
}
start_docker() {
echo -e "🐳 Starting Docker ...\n"
if [[ $os == "Mac" ]]; then
open --background -a Docker && while ! docker system info > /dev/null 2>&1; do sleep 1; done
else
if ! $sudo_cmd systemctl is-active docker.service > /dev/null; then
echo "Starting docker service"
$sudo_cmd systemctl start docker.service
fi
if [[ -z $sudo_cmd ]]; then
if ! docker ps > /dev/null && true; then
request_sudo
fi
fi
fi
}
wait_for_containers_start() {
local timeout=$1
# The while loop is important because for-loops don't work for dynamic values
while [[ $timeout -gt 0 ]]; do
status_code="$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8080/api/v1/health?live=1" || true)"
if [[ status_code -eq 200 ]]; then
break
else
echo -ne "Waiting for all containers to start. This check will timeout in $timeout seconds ...\r\c"
fi
((timeout--))
sleep 1
done
echo ""
}
bye() { # Prints a friendly good bye message and exits the script.
# Switch back to the original directory
popd > /dev/null 2>&1
if [[ "$?" -ne 0 ]]; then
set +o errexit
echo "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:"
echo ""
echo -e "cd ${DOCKER_STANDALONE_DIR}"
echo -e "$sudo_cmd $docker_compose_cmd ps -a"
echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack"
echo "++++++++++++++++++++++++++++++++++++++++"
if [[ $email == "" ]]; then
echo -e "\n📨 Please share your email to receive support with the installation"
read -rp 'Email: ' email
while [[ $email == "" ]]
do
read -rp 'Email: ' email
done
fi
send_event "installation_support"
echo ""
echo -e "\nWe will reach out to you at the email provided shortly, Exiting for now. Bye! 👋 \n"
exit 0
fi
}
request_sudo() {
if hash sudo 2>/dev/null; then
echo -e "\n\n🙇 We will need sudo access to complete the installation."
if (( $EUID != 0 )); then
sudo_cmd="sudo"
echo -e "Please enter your sudo password, if prompted."
if ! $sudo_cmd -l | grep -e "NOPASSWD: ALL" > /dev/null && ! $sudo_cmd -v; then
echo "Need sudo privileges to proceed with the installation."
exit 1;
fi
echo -e "Got it! Thanks!! 🙏\n"
echo -e "Okay! We will bring up the SigNoz cluster from here 🚀\n"
fi
fi
}
echo ""
echo -e "👋 Thank you for trying out SigNoz!"
echo ""
echo -e "SigNoz now installs and runs through ${Green}Foundry${NC}."
echo ""
echo -e "${Yellow}⚠️ This install script has been deprecated and is no longer maintained.${NC}"
echo -e "${Yellow}⚠️ Please see https://github.com/SigNoz/signoz/blob/main/deploy/README.md for new installation and migrations to Foundry.${NC}"
echo ""
echo ""
echo -e "Please follow the latest installation instructions here:"
echo -e "${Green}👉 https://signoz.io/docs/install/docker/${NC}"
echo ""
echo -e "🙏 Thank you!"
echo -e "👋 Thank you for trying out SigNoz! "
echo ""
exit 0
sudo_cmd=""
docker_compose_cmd=""
# Check sudo permissions
if (( $EUID != 0 )); then
echo "🟡 Running installer with non-sudo permissions."
echo " In case of any failure or prompt, please consider running the script with sudo privileges."
echo ""
else
sudo_cmd="sudo"
fi
# Checking OS and assigning package manager
desired_os=0
os=""
email=""
echo -e "🌏 Detecting your OS ...\n"
check_os
# Obtain unique installation id
# sysinfo="$(uname -a)"
# if [[ $? -ne 0 ]]; then
# uuid="$(uuidgen)"
# uuid="${uuid:-$(cat /proc/sys/kernel/random/uuid)}"
# sysinfo="${uuid:-$(cat /proc/sys/kernel/random/uuid)}"
# fi
if ! sysinfo="$(uname -a)"; then
uuid="$(uuidgen)"
uuid="${uuid:-$(cat /proc/sys/kernel/random/uuid)}"
sysinfo="${uuid:-$(cat /proc/sys/kernel/random/uuid)}"
fi
digest_cmd=""
if hash shasum 2>/dev/null; then
digest_cmd="shasum -a 256"
elif hash sha256sum 2>/dev/null; then
digest_cmd="sha256sum"
elif hash openssl 2>/dev/null; then
digest_cmd="openssl dgst -sha256"
fi
if [[ -z $digest_cmd ]]; then
SIGNOZ_INSTALLATION_ID="$sysinfo"
else
SIGNOZ_INSTALLATION_ID=$(echo "$sysinfo" | $digest_cmd | grep -E -o '[a-zA-Z0-9]{64}')
fi
setup_type='clickhouse'
# Run bye if failure happens
trap bye EXIT
URL="https://api.segment.io/v1/track"
HEADER_1="Content-Type: application/json"
HEADER_2="Authorization: Basic OWtScko3b1BDR1BFSkxGNlFqTVBMdDVibGpGaFJRQnI="
send_event() {
error=""
case "$1" in
'install_started')
event="Installation Started"
;;
'os_not_supported')
event="Installation Error"
error="OS Not Supported"
;;
'docker_not_installed')
event="Installation Error"
error="Docker not installed"
;;
'docker_compose_not_found')
event="Installation Error"
event="Docker Compose not found"
;;
'port_not_available')
event="Installation Error"
error="port not available"
;;
'installation_error_checks')
event="Installation Error - Checks"
error="Containers not started"
others='"data": "some_checks",'
;;
'installation_support')
event="Installation Support"
others='"email": "'"$email"'",'
;;
'installation_success')
event="Installation Success"
;;
'identify_successful_installation')
event="Identify Successful Installation"
others='"email": "'"$email"'",'
;;
*)
print_error "unknown event type: $1"
exit 1
;;
esac
if [[ "$error" != "" ]]; then
error='"error": "'"$error"'", '
fi
DATA='{ "anonymousId": "'"$SIGNOZ_INSTALLATION_ID"'", "event": "'"$event"'", "properties": { "os": "'"$os"'", '"$error $others"' "setup_type": "'"$setup_type"'" } }'
if has_curl; then
curl -sfL -d "$DATA" --header "$HEADER_1" --header "$HEADER_2" "$URL" > /dev/null 2>&1
elif has_wget; then
wget -q --post-data="$DATA" --header "$HEADER_1" --header "$HEADER_2" "$URL" > /dev/null 2>&1
fi
}
send_event "install_started"
if [[ $desired_os -eq 0 ]]; then
send_event "os_not_supported"
fi
# Check is Docker daemon is installed and available. If not, the install & start Docker for Linux machines. We cannot automatically install Docker Desktop on Mac OS
if ! is_command_present docker; then
if [[ $package_manager == "apt-get" || $package_manager == "zypper" || $package_manager == "yum" ]]; then
request_sudo
install_docker
# enable docker without sudo from next reboot
sudo usermod -aG docker "${USER}"
elif is_mac; then
echo ""
echo "+++++++++++ IMPORTANT READ ++++++++++++++++++++++"
echo "Docker Desktop must be installed manually on Mac OS to proceed. Docker can only be installed automatically on Ubuntu / openSUSE / SLES / Redhat / Cent OS"
echo "https://docs.docker.com/docker-for-mac/install/"
echo "++++++++++++++++++++++++++++++++++++++++++++++++"
send_event "docker_not_installed"
exit 1
else
echo ""
echo "+++++++++++ IMPORTANT READ ++++++++++++++++++++++"
echo "Docker must be installed manually on your machine to proceed. Docker can only be installed automatically on Ubuntu / openSUSE / SLES / Redhat / Cent OS"
echo "https://docs.docker.com/get-docker/"
echo "++++++++++++++++++++++++++++++++++++++++++++++++"
send_event "docker_not_installed"
exit 1
fi
fi
if has_docker_compose_plugin; then
echo "docker compose plugin is present, using it"
docker_compose_cmd="docker compose"
# Install docker-compose
else
docker_compose_cmd="docker-compose"
if ! is_command_present docker-compose; then
request_sudo
install_docker_compose
fi
fi
start_docker
# Switch to the Docker Standalone directory
pushd "${BASE_DIR}/${DOCKER_STANDALONE_DIR}" > /dev/null 2>&1
# check for open ports, if signoz is not installed
if is_command_present docker-compose; then
if $sudo_cmd $docker_compose_cmd ps | grep "signoz" | grep -q "healthy" > /dev/null 2>&1; then
echo "SigNoz already installed, skipping the occupied ports check"
else
check_ports_occupied
fi
fi
echo ""
echo -e "\n🟡 Pulling the latest container images for SigNoz.\n"
$sudo_cmd $docker_compose_cmd pull
echo ""
echo "🟡 Starting the SigNoz containers. It may take a few minutes ..."
echo
# The $docker_compose_cmd command does some nasty stuff for the `--detach` functionality. So we add a `|| true` so that the
# script doesn't exit because this command looks like it failed to do it's thing.
$sudo_cmd $docker_compose_cmd up --detach --remove-orphans || true
wait_for_containers_start 60
echo ""
if [[ $status_code -ne 200 ]]; then
echo "+++++++++++ ERROR ++++++++++++++++++++++"
echo "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:"
echo ""
echo "cd ${DOCKER_STANDALONE_DIR}"
echo "$sudo_cmd $docker_compose_cmd ps -a"
echo ""
echo "Try bringing down the containers and retrying the installation"
echo "cd ${DOCKER_STANDALONE_DIR}"
echo "$sudo_cmd $docker_compose_cmd down -v"
echo ""
echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
echo "or reach us on SigNoz for support https://signoz.io/slack"
echo "++++++++++++++++++++++++++++++++++++++++"
send_event "installation_error_checks"
exit 1
else
send_event "installation_success"
echo "++++++++++++++++++ SUCCESS ++++++++++++++++++++++"
echo ""
echo "🟢 Your installation is complete!"
echo ""
echo -e "🟢 SigNoz is running on http://localhost:8080"
echo ""
echo " By default, retention period is set to 15 days for logs and traces, and 30 days for metrics."
echo -e "To change this, navigate to the General tab on the Settings page of SigNoz UI. For more details, refer to https://signoz.io/docs/userguide/retention-period \n"
echo " To bring down SigNoz and clean volumes:"
echo ""
echo "cd ${DOCKER_STANDALONE_DIR}"
echo "$sudo_cmd $docker_compose_cmd down -v"
echo ""
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
echo ""
echo "👉 Need help in Getting Started?"
echo -e "Join us on Slack https://signoz.io/slack"
echo ""
echo -e "\n📨 Please share your email to receive support & updates about SigNoz!"
read -rp 'Email: ' email
while [[ $email == "" ]]
do
read -rp 'Email: ' email
done
send_event "identify_successful_installation"
fi
echo -e "\n🙏 Thank you!\n"

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
{
"title": "WebSettings",
"required": [
"posthog",
"appcues",

View File

@@ -1,52 +0,0 @@
package implmetricreductionrule
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/metricreductionrule"
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
)
type module struct{}
func NewModule() metricreductionrule.Module {
return &module{}
}
var errNotImplemented = errors.New(errors.TypeUnsupported, metricreductionruletypes.ErrCodeMetricReductionRuleUnsupported,
"metric volume control is not yet implemented")
func (m *module) List(_ context.Context, _ valuer.UUID, _ *metricreductionruletypes.ListReductionRulesParams) (*metricreductionruletypes.GettableReductionRules, error) {
return nil, errNotImplemented
}
func (m *module) Create(_ context.Context, _ valuer.UUID, _ string, _ *metricreductionruletypes.PostableReductionRule) (*metricreductionruletypes.GettableReductionRule, error) {
return nil, errNotImplemented
}
func (m *module) GetByID(_ context.Context, _ valuer.UUID, _ valuer.UUID) (*metricreductionruletypes.GettableReductionRule, error) {
return nil, errNotImplemented
}
func (m *module) UpdateByID(_ context.Context, _ valuer.UUID, _ string, _ valuer.UUID, _ *metricreductionruletypes.UpdatableReductionRule) (*metricreductionruletypes.GettableReductionRule, error) {
return nil, errNotImplemented
}
func (m *module) DeleteByID(_ context.Context, _ valuer.UUID, _ valuer.UUID) error {
return errNotImplemented
}
func (m *module) Preview(_ context.Context, _ valuer.UUID, _ *metricreductionruletypes.PostableReductionRulePreview) (*metricreductionruletypes.GettableReductionRulePreview, error) {
return nil, errNotImplemented
}
func (m *module) Stats(_ context.Context, _ valuer.UUID) (*metricreductionruletypes.GettableReductionRuleStats, error) {
return nil, errNotImplemented
}
func (m *module) Timeseries(_ context.Context, _ valuer.UUID) (*querybuildertypesv5.QueryRangeResponse, error) {
return nil, errNotImplemented
}

View File

@@ -107,15 +107,6 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
metricsReduction := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableMetricsReduction, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureEnableMetricsReduction.String()),
Active: metricsReduction,
Usage: 0,
UsageLimit: -1,
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {

View File

@@ -29,18 +29,6 @@ if (!HTMLElement.prototype.scrollIntoView) {
HTMLElement.prototype.scrollIntoView = function (): void {};
}
// jsdom doesn't implement the Pointer Capture API, which Radix UI primitives
// (e.g. @signozhq/ui Select) call when opening. Stub them so those components
// can be exercised in tests.
if (!HTMLElement.prototype.hasPointerCapture) {
HTMLElement.prototype.hasPointerCapture = function (): boolean {
return false;
};
}
if (!HTMLElement.prototype.releasePointerCapture) {
HTMLElement.prototype.releasePointerCapture = function (): void {};
}
if (typeof window.IntersectionObserver === 'undefined') {
class IntersectionObserverMock {
observe(): void {}

View File

@@ -25,7 +25,7 @@
"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 ./src/schemas/generated/webSettings.schema.json -o src/types/generated/webSettings.ts --style.useTabs --style.tabWidth=1 --style.singleQuote --bannerComment '/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM frontend/src/schemas/generated/webSettings.schema.json */'"
"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 */'"
},
"engines": {
"node": ">=22.0.0",

View File

@@ -122,13 +122,6 @@ export const DashboardWidget = Loadable(
import(/* webpackChunkName: "DashboardWidgetPage" */ 'pages/DashboardWidget'),
);
export const DashboardPanelEditorPage = Loadable(
() =>
import(
/* webpackChunkName: "DashboardPanelEditorPage" */ 'pages/DashboardPageV2/PanelEditorPage/PanelEditorPage'
),
);
export const EditRulesPage = Loadable(
() => import(/* webpackChunkName: "Alerts Edit Page" */ 'pages/EditRules'),
);

View File

@@ -11,7 +11,6 @@ import {
ChannelsNew,
CreateNewAlerts,
DashboardPage,
DashboardPanelEditorPage,
DashboardsListPage,
DashboardWidget,
EditRulesPage,
@@ -197,13 +196,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'DASHBOARD_WIDGET',
},
{
path: ROUTES.DASHBOARD_PANEL_EDITOR,
exact: true,
component: DashboardPanelEditorPage,
isPrivate: true,
key: 'DASHBOARD_PANEL_EDITOR',
},
{
path: ROUTES.EDIT_ALERTS,
exact: true,

View File

@@ -23,6 +23,7 @@ import type {
GetLLMPricingRulePathParameters,
ListLLMPricingRules200,
ListLLMPricingRulesParams,
ListUnmappedLLMModels200,
LlmpricingruletypesUpdatableLLMPricingRulesDTO,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
@@ -393,3 +394,87 @@ export const invalidateGetLLMPricingRule = async (
return queryClient;
};
/**
* Returns models seen in the last hour of trace data (gen_ai.request.model) that no pricing rule pattern matches, so the user can add them to an existing rule or create a new one.
* @summary List unmapped models
*/
export const listUnmappedLLMModels = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListUnmappedLLMModels200>({
url: `/api/v1/llm_pricing_rules/unmapped_models`,
method: 'GET',
signal,
});
};
export const getListUnmappedLLMModelsQueryKey = () => {
return [`/api/v1/llm_pricing_rules/unmapped_models`] as const;
};
export const getListUnmappedLLMModelsQueryOptions = <
TData = Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListUnmappedLLMModelsQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listUnmappedLLMModels>>
> = ({ signal }) => listUnmappedLLMModels(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListUnmappedLLMModelsQueryResult = NonNullable<
Awaited<ReturnType<typeof listUnmappedLLMModels>>
>;
export type ListUnmappedLLMModelsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List unmapped models
*/
export function useListUnmappedLLMModels<
TData = Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListUnmappedLLMModelsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List unmapped models
*/
export const invalidateListUnmappedLLMModels = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListUnmappedLLMModelsQueryKey() },
options,
);
return queryClient;
};

File diff suppressed because it is too large Load Diff

View File

@@ -2094,45 +2094,6 @@ export interface AuthtypesGettableTokenDTO {
tokenType?: string;
}
export enum CoretypesKindDTO {
anonymous = 'anonymous',
organization = 'organization',
role = 'role',
serviceaccount = 'serviceaccount',
user = 'user',
'notification-channel' = 'notification-channel',
'route-policy' = 'route-policy',
'apdex-setting' = 'apdex-setting',
'auth-domain' = 'auth-domain',
session = 'session',
'cloud-integration' = 'cloud-integration',
'cloud-integration-service' = 'cloud-integration-service',
integration = 'integration',
dashboard = 'dashboard',
'public-dashboard' = 'public-dashboard',
'ingestion-key' = 'ingestion-key',
'ingestion-limit' = 'ingestion-limit',
pipeline = 'pipeline',
'user-preference' = 'user-preference',
'org-preference' = 'org-preference',
'quick-filter' = 'quick-filter',
'ttl-setting' = 'ttl-setting',
rule = 'rule',
'planned-maintenance' = 'planned-maintenance',
'saved-view' = 'saved-view',
'trace-funnel' = 'trace-funnel',
'factor-password' = 'factor-password',
'factor-api-key' = 'factor-api-key',
license = 'license',
subscription = 'subscription',
logs = 'logs',
traces = 'traces',
metrics = 'metrics',
'audit-logs' = 'audit-logs',
'meter-metrics' = 'meter-metrics',
'logs-field' = 'logs-field',
'traces-field' = 'traces-field',
}
export enum CoretypesTypeDTO {
user = 'user',
serviceaccount = 'serviceaccount',
@@ -2143,7 +2104,10 @@ export enum CoretypesTypeDTO {
telemetryresource = 'telemetryresource',
}
export interface CoretypesResourceRefDTO {
kind: CoretypesKindDTO;
/**
* @type string
*/
kind: string;
type: CoretypesTypeDTO;
}
@@ -2279,12 +2243,12 @@ export interface AuthtypesPostableRoleDTO {
/**
* @type string
*/
description?: string;
description: string;
/**
* @type string
*/
name: string;
transactionGroups?: AuthtypesTransactionGroupsDTO;
transactionGroups: AuthtypesTransactionGroupsDTO;
}
export interface AuthtypesPostableRotateTokenDTO {
@@ -2317,7 +2281,7 @@ export interface AuthtypesPostableUserDTO {
/**
* @type array
*/
userRoles?: AuthtypesPostableUserRoleDTO[];
userRoles: AuthtypesPostableUserRoleDTO[];
}
export interface AuthtypesRoleDTO {
@@ -6649,6 +6613,29 @@ export interface LlmpricingruletypesGettablePricingRulesDTO {
total: number;
}
export interface LlmpricingruletypesUnmappedModelDTO {
/**
* @type string
*/
modelName: string;
/**
* @type string
*/
provider?: string;
/**
* @type integer
* @minimum 0
*/
spanCount: number;
}
export interface LlmpricingruletypesGettableUnmappedModelsDTO {
/**
* @type array,null
*/
items: LlmpricingruletypesUnmappedModelDTO[] | null;
}
export interface LlmpricingruletypesUpdatableLLMPricingRuleDTO {
/**
* @type boolean
@@ -6689,213 +6676,6 @@ export interface LlmpricingruletypesUpdatableLLMPricingRulesDTO {
rules: LlmpricingruletypesUpdatableLLMPricingRuleDTO[] | null;
}
export enum MetricreductionruletypesAssetTypeDTO {
dashboard = 'dashboard',
alert_rule = 'alert_rule',
}
export interface MetricreductionruletypesAffectedWidgetDTO {
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
}
export interface MetricreductionruletypesAffectedAssetDTO {
/**
* @type string
*/
id: string;
/**
* @type array,null
*/
impactedLabels: string[] | null;
/**
* @type string
*/
name: string;
type: MetricreductionruletypesAssetTypeDTO;
widget?: MetricreductionruletypesAffectedWidgetDTO;
}
export enum MetricreductionruletypesMatchTypeDTO {
drop = 'drop',
keep = 'keep',
}
export interface MetricreductionruletypesGettableReductionRuleDTO {
/**
* @type boolean
*/
active: boolean;
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
* @format date-time
*/
effectiveFrom: string;
/**
* @type string
*/
id: string;
/**
* @type integer
* @minimum 0
*/
ingestedSeries: number;
/**
* @type array,null
*/
labels: string[] | null;
matchType: MetricreductionruletypesMatchTypeDTO;
/**
* @type string
*/
metricName: string;
/**
* @type number
* @format double
*/
reductionPercent: number;
/**
* @type integer
* @minimum 0
*/
retainedSeries: number;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface MetricreductionruletypesGettableReductionRulePreviewDTO {
/**
* @type array,null
*/
affectedAssets: MetricreductionruletypesAffectedAssetDTO[] | null;
/**
* @type integer
* @minimum 0
*/
currentRetainedSeries: number;
/**
* @type array,null
*/
droppedLabels: string[] | null;
/**
* @type string
* @format date-time
*/
effectiveFrom: string;
/**
* @type integer
* @minimum 0
*/
ingestedSeries: number;
/**
* @type number
* @format double
*/
reductionPercent: number;
/**
* @type integer
* @minimum 0
*/
retainedSeries: number;
}
export interface MetricreductionruletypesGettableReductionRuleStatsDTO {
/**
* @type number
* @format double
*/
estimatedMonthlySavingsUsd: number;
/**
* @type integer
* @minimum 0
*/
ingestedSeries: number;
/**
* @type integer
* @minimum 0
*/
retainedSeries: number;
}
export interface MetricreductionruletypesGettableReductionRulesDTO {
/**
* @type array,null
*/
rules: MetricreductionruletypesGettableReductionRuleDTO[] | null;
/**
* @type integer
*/
total: number;
}
export enum MetricreductionruletypesOrderDTO {
asc = 'asc',
desc = 'desc',
}
export interface MetricreductionruletypesPostableReductionRuleDTO {
/**
* @type array,null
*/
labels: string[] | null;
matchType: MetricreductionruletypesMatchTypeDTO;
/**
* @type string
*/
metricName: string;
}
export interface MetricreductionruletypesPostableReductionRulePreviewDTO {
/**
* @type array,null
*/
labels: string[] | null;
/**
* @type integer
* @format int64
*/
lookbackMs?: number;
matchType: MetricreductionruletypesMatchTypeDTO;
/**
* @type string
*/
metricName: string;
}
export enum MetricreductionruletypesReductionRuleOrderByDTO {
metric = 'metric',
ingested_volume = 'ingested_volume',
reduced_volume = 'reduced_volume',
reduction = 'reduction',
last_updated = 'last_updated',
}
export interface MetricreductionruletypesUpdatableReductionRuleDTO {
/**
* @type array,null
*/
labels: string[] | null;
matchType: MetricreductionruletypesMatchTypeDTO;
}
export interface MetricsexplorertypesInspectMetricsRequestDTO {
/**
* @type integer
@@ -9803,6 +9583,14 @@ export type GetLLMPricingRule200 = {
status: string;
};
export type ListUnmappedLLMModels200 = {
data: LlmpricingruletypesGettableUnmappedModelsDTO;
/**
* @type string
*/
status: string;
};
export type ListPromotedAndIndexedPaths200 = {
/**
* @type array,null
@@ -10165,7 +9953,7 @@ export type ListUsersDeprecated200 = {
status: string;
};
export type DeleteUserDeprecatedPathParameters = {
export type DeleteUserPathParameters = {
id: string;
};
export type GetUserDeprecatedPathParameters = {
@@ -10541,102 +10329,6 @@ export type Livez200 = {
status: string;
};
export type ListMetricReductionRulesParams = {
/**
* @description undefined
*/
orderBy?: MetricreductionruletypesReductionRuleOrderByDTO;
/**
* @description undefined
*/
order?: MetricreductionruletypesOrderDTO;
/**
* @type string
* @description undefined
*/
search?: string;
/**
* @type string
* @description undefined
*/
metricName?: string;
/**
* @type integer
* @description undefined
*/
offset?: number;
/**
* @type integer
* @description undefined
*/
limit?: number;
};
export type ListMetricReductionRules200 = {
data: MetricreductionruletypesGettableReductionRulesDTO;
/**
* @type string
*/
status: string;
};
export type CreateMetricReductionRule201 = {
data: MetricreductionruletypesGettableReductionRuleDTO;
/**
* @type string
*/
status: string;
};
export type DeleteMetricReductionRuleByIDPathParameters = {
id: string;
};
export type GetMetricReductionRuleByIDPathParameters = {
id: string;
};
export type GetMetricReductionRuleByID200 = {
data: MetricreductionruletypesGettableReductionRuleDTO;
/**
* @type string
*/
status: string;
};
export type UpdateMetricReductionRuleByIDPathParameters = {
id: string;
};
export type UpdateMetricReductionRuleByID200 = {
data: MetricreductionruletypesGettableReductionRuleDTO;
/**
* @type string
*/
status: string;
};
export type PreviewMetricReductionRule200 = {
data: MetricreductionruletypesGettableReductionRulePreviewDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricReductionRuleStats200 = {
data: MetricreductionruletypesGettableReductionRuleStatsDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricReductionRuleTimeseries200 = {
data: Querybuildertypesv5QueryRangeResponseDTO;
/**
* @type string
*/
status: string;
};
export type ListMetricsParams = {
/**
* @type integer,null
@@ -10673,14 +10365,9 @@ export type ListMetrics200 = {
status: string;
};
export type GetMetricAlertsParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
export type GetMetricAlertsPathParameters = {
metricName: string;
};
export type GetMetricAlerts200 = {
data: MetricsexplorertypesMetricAlertsResponseDTO;
/**
@@ -10689,20 +10376,18 @@ export type GetMetricAlerts200 = {
status: string;
};
export type GetMetricAttributesPathParameters = {
metricName: string;
};
export type GetMetricAttributesParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
/**
* @type integer,null
* @description Start of the time range as a Unix timestamp in milliseconds.
* @description undefined
*/
start?: number | null;
/**
* @type integer,null
* @description End of the time range as a Unix timestamp in milliseconds.
* @description undefined
*/
end?: number | null;
};
@@ -10715,14 +10400,9 @@ export type GetMetricAttributes200 = {
status: string;
};
export type GetMetricDashboardsParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
export type GetMetricDashboardsPathParameters = {
metricName: string;
};
export type GetMetricDashboards200 = {
data: MetricsexplorertypesMetricDashboardsResponseDTO;
/**
@@ -10731,14 +10411,9 @@ export type GetMetricDashboards200 = {
status: string;
};
export type GetMetricHighlightsParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
export type GetMetricHighlightsPathParameters = {
metricName: string;
};
export type GetMetricHighlights200 = {
data: MetricsexplorertypesMetricHighlightsResponseDTO;
/**
@@ -10747,24 +10422,22 @@ export type GetMetricHighlights200 = {
status: string;
};
export type InspectMetrics200 = {
data: MetricsexplorertypesInspectMetricsResponseDTO;
export type GetMetricMetadataPathParameters = {
metricName: string;
};
export type GetMetricMetadata200 = {
data: MetricsexplorertypesMetricMetadataDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricMetadataParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
export type UpdateMetricMetadataPathParameters = {
metricName: string;
};
export type GetMetricMetadata200 = {
data: MetricsexplorertypesMetricMetadataDTO;
export type InspectMetrics200 = {
data: MetricsexplorertypesInspectMetricsResponseDTO;
/**
* @type string
*/
@@ -11199,9 +10872,6 @@ export type CreateUser201 = {
status: string;
};
export type DeleteUserPathParameters = {
id: string;
};
export type GetUserPathParameters = {
id: string;
};

View File

@@ -23,7 +23,6 @@ import type {
CreateResetPasswordToken201,
CreateResetPasswordTokenPathParameters,
CreateUser201,
DeleteUserDeprecatedPathParameters,
DeleteUserPathParameters,
GetMyUser200,
GetMyUserDeprecated200,
@@ -512,11 +511,10 @@ export const invalidateListUsersDeprecated = async (
/**
* This endpoint deletes the user by id
* @deprecated
* @summary Delete user
*/
export const deleteUserDeprecated = (
{ id }: DeleteUserDeprecatedPathParameters,
export const deleteUser = (
{ id }: DeleteUserPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
@@ -526,23 +524,23 @@ export const deleteUserDeprecated = (
});
};
export const getDeleteUserDeprecatedMutationOptions = <
export const getDeleteUserMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
Awaited<ReturnType<typeof deleteUser>>,
TError,
{ pathParams: DeleteUserDeprecatedPathParameters },
{ pathParams: DeleteUserPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
Awaited<ReturnType<typeof deleteUser>>,
TError,
{ pathParams: DeleteUserDeprecatedPathParameters },
{ pathParams: DeleteUserPathParameters },
TContext
> => {
const mutationKey = ['deleteUserDeprecated'];
const mutationKey = ['deleteUser'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
@@ -552,45 +550,43 @@ export const getDeleteUserDeprecatedMutationOptions = <
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
{ pathParams: DeleteUserDeprecatedPathParameters }
Awaited<ReturnType<typeof deleteUser>>,
{ pathParams: DeleteUserPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteUserDeprecated(pathParams);
return deleteUser(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteUserDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteUserDeprecated>>
export type DeleteUserMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteUser>>
>;
export type DeleteUserDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
export type DeleteUserMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Delete user
*/
export const useDeleteUserDeprecated = <
export const useDeleteUser = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
Awaited<ReturnType<typeof deleteUser>>,
TError,
{ pathParams: DeleteUserDeprecatedPathParameters },
{ pathParams: DeleteUserPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
Awaited<ReturnType<typeof deleteUser>>,
TError,
{ pathParams: DeleteUserDeprecatedPathParameters },
{ pathParams: DeleteUserPathParameters },
TContext
> => {
return useMutation(getDeleteUserDeprecatedMutationOptions(options));
return useMutation(getDeleteUserMutationOptions(options));
};
/**
* This endpoint returns the user by id
@@ -1313,85 +1309,6 @@ export const useCreateUser = <
> => {
return useMutation(getCreateUserMutationOptions(options));
};
/**
* This endpoint deletes the user by id
* @summary Delete user
*/
export const deleteUser = (
{ id }: DeleteUserPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/users/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteUserMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUser>>,
TError,
{ pathParams: DeleteUserPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteUser>>,
TError,
{ pathParams: DeleteUserPathParameters },
TContext
> => {
const mutationKey = ['deleteUser'];
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 deleteUser>>,
{ pathParams: DeleteUserPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteUser(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteUserMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteUser>>
>;
export type DeleteUserMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete user
*/
export const useDeleteUser = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUser>>,
TError,
{ pathParams: DeleteUserPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteUser>>,
TError,
{ pathParams: DeleteUserPathParameters },
TContext
> => {
return useMutation(getDeleteUserMutationOptions(options));
};
/**
* This endpoint returns the user by id
* @summary Get user by user id

View File

@@ -44,5 +44,4 @@ export enum LOCALSTORAGE {
ACTIVE_SIGNOZ_INSTANCE_URL = 'ACTIVE_SIGNOZ_INSTANCE_URL',
DASHBOARDS_LIST_VISIBLE_COLUMNS = 'DASHBOARDS_LIST_VISIBLE_COLUMNS',
DASHBOARDS_LIST_VIEWS = 'DASHBOARDS_LIST_VIEWS',
DASHBOARD_V2_PANEL_COLUMN_WIDTHS = 'DASHBOARD_V2_PANEL_COLUMN_WIDTHS',
}

View File

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

View File

@@ -408,9 +408,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isPublicDashboard = pathname.startsWith('/public/dashboard/');
const isAIAssistantPage = pathname.startsWith('/ai-assistant/');
// The V2 panel editor is a chromeless full-page route (no side nav / top nav),
// like the onboarding and public-dashboard screens.
const isPanelEditorV2 = routeKey === 'DASHBOARD_PANEL_EDITOR';
const renderFullScreen =
pathname === ROUTES.GET_STARTED ||
@@ -421,8 +418,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
pathname === ROUTES.GET_STARTED_AWS_MONITORING ||
pathname === ROUTES.GET_STARTED_AZURE_MONITORING ||
isPublicDashboard ||
isPanelEditorV2;
isPublicDashboard;
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);

View File

@@ -7,17 +7,15 @@
&--legend-right {
flex-direction: row;
.chart-layout__legend-wrapper {
padding-left: 0 !important;
}
}
&__legend-wrapper {
// The inline height is the legend rectangle from calculateChartDimensions;
// border-box keeps the padding inside it so the wrapper doesn't grow past
// that height and steal space from the chart. overflow:hidden clips to the
// rectangle so the virtualized legend scrolls within it.
box-sizing: border-box;
min-height: 0;
overflow: hidden;
padding-left: 12px;
padding-bottom: 12px;
overflow: auto;
}
}

View File

@@ -191,6 +191,9 @@ function TimeSeries({
if (metrics[0] && yAxisUnit) {
updateMetricMetadata(
{
pathParams: {
metricName: metricNames[0],
},
data: buildUpdateMetricYAxisUnitPayload(
metricNames[0],
metrics[0],

View File

@@ -48,14 +48,18 @@ function AllAttributes({
isLoading: isLoadingAttributes,
isError: isErrorAttributes,
refetch: refetchAttributes,
} = useGetMetricAttributes({
metricName,
start: minTime ? Math.floor(minTime / 1000000) : undefined,
end: maxTime ? Math.floor(maxTime / 1000000) : undefined,
});
} = useGetMetricAttributes(
{
metricName,
},
{
start: minTime ? Math.floor(minTime / 1000000) : undefined,
end: maxTime ? Math.floor(maxTime / 1000000) : undefined,
},
);
const attributes = useMemo(
() => attributesData?.data.attributes ?? [],
() => attributesData?.data?.attributes ?? [],
[attributesData],
);

View File

@@ -237,6 +237,9 @@ function Metadata({
const handleSave = useCallback(() => {
updateMetricMetadata(
{
pathParams: {
metricName,
},
data: transformUpdateMetricMetadataRequest(metricName, metricMetadataState),
},
{

View File

@@ -56,7 +56,7 @@ function MetricDetails({
);
const metadata = useMemo(() => {
if (!metricMetadataResponse) {
if (!metricMetadataResponse?.data) {
return null;
}
const { type, description, unit, temporality, isMonotonic } =

View File

@@ -195,12 +195,14 @@ describe('Metadata', () => {
expect(mockUseUpdateMetricMetadata).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
metricName: MOCK_METRIC_NAME,
type: MetrictypesTypeDTO.sum,
temporality: MetrictypesTemporalityDTO.cumulative,
unit: 'By',
isMonotonic: true,
}),
pathParams: {
metricName: MOCK_METRIC_NAME,
},
}),
expect.objectContaining({
onSuccess: expect.any(Function),

View File

@@ -1,5 +1,4 @@
import type {
CoretypesKindDTO,
CoretypesObjectGroupDTO,
CoretypesTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
@@ -57,7 +56,7 @@ const baseAuthzResources: AuthzResources = {
// API payload resource refs — only kind+type, no allowedVerbs (matches CoretypesResourceRefDTO shape)
const dashboardResourceRef = {
kind: 'dashboard' as CoretypesKindDTO,
kind: 'dashboard',
type: 'metaresource' as CoretypesTypeDTO,
};
const alertResourceRef = {

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Badge } from '@signozhq/ui/badge';
import type {
CoretypesKindDTO,
CoretypesObjectGroupDTO,
CoretypesResourceRefDTO,
CoretypesTypeDTO,
@@ -148,7 +147,7 @@ export function buildPatchPayload({
continue;
}
const resourceDef: CoretypesResourceRefDTO = {
kind: found.kind as CoretypesKindDTO,
kind: found.kind,
type: found.type as CoretypesTypeDTO,
};

View File

@@ -1,61 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { useConfirmableAction } from '../useConfirmableAction';
describe('useConfirmableAction', () => {
it('starts closed and idle', () => {
const { result } = renderHook(() =>
useConfirmableAction(jest.fn().mockResolvedValue(undefined)),
);
expect(result.current.open).toBe(false);
expect(result.current.isPending).toBe(false);
});
it('request() opens the prompt without running the action', () => {
const action = jest.fn().mockResolvedValue(undefined);
const { result } = renderHook(() => useConfirmableAction(action));
act(() => result.current.request());
expect(result.current.open).toBe(true);
expect(action).not.toHaveBeenCalled();
});
it('confirm() runs the action and closes on success', async () => {
const action = jest.fn().mockResolvedValue(undefined);
const { result } = renderHook(() => useConfirmableAction(action));
act(() => result.current.request());
await act(async () => {
await result.current.confirm();
});
expect(action).toHaveBeenCalledTimes(1);
expect(result.current.open).toBe(false);
expect(result.current.isPending).toBe(false);
});
it('keeps the prompt open and resets pending when the action rejects', async () => {
const action = jest.fn().mockRejectedValue(new Error('boom'));
const { result } = renderHook(() => useConfirmableAction(action));
act(() => result.current.request());
await act(async () => {
await expect(result.current.confirm()).rejects.toThrow('boom');
});
expect(result.current.open).toBe(true);
expect(result.current.isPending).toBe(false);
});
it('cancel() closes the prompt without running the action', () => {
const action = jest.fn().mockResolvedValue(undefined);
const { result } = renderHook(() => useConfirmableAction(action));
act(() => result.current.request());
act(() => result.current.cancel());
expect(result.current.open).toBe(false);
expect(action).not.toHaveBeenCalled();
});
});

View File

@@ -2,13 +2,13 @@ import {
AuthtypesTransactionDTO,
CoretypesTypeDTO,
AuthtypesRelationDTO,
CoretypesKindDTO,
} from '../../api/generated/services/sigNoz.schemas';
import permissionsType from './permissions.type';
import {
AuthZObject,
AuthZRelation,
BrandedPermission,
ResourceName,
ResourcesForRelation,
ResourceType,
} from './types';
@@ -87,7 +87,7 @@ export function permissionToTransactionDto(
relation: relation as AuthtypesRelationDTO,
object: {
resource: {
kind: resourceName as CoretypesKindDTO,
kind: resourceName as ResourceName,
type: type as CoretypesTypeDTO,
},
selector: selector || '*',

View File

@@ -1,45 +0,0 @@
import { useCallback, useMemo, useState } from 'react';
export interface ConfirmableAction {
/** Whether the confirmation prompt is open. */
open: boolean;
/** The confirmed action is in flight. */
isPending: boolean;
/** Open the confirmation prompt (e.g. from a menu item / button). */
request: () => void;
/** Run the action, tracking the in-flight flag; closes the prompt on success. */
confirm: () => Promise<void>;
/** Dismiss the prompt without acting. */
cancel: () => void;
}
/**
* Generic two-step confirm flow for a (usually destructive) async action.
* `request()` opens the prompt, `confirm()` runs `action` while tracking an
* in-flight flag and closes on success, `cancel()` dismisses it. Owns only the
* confirm state machine — what renders the prompt (dialog, popover) is the
* caller's concern, so it stays reusable across confirm surfaces.
*/
export function useConfirmableAction(
action: () => Promise<void>,
): ConfirmableAction {
const [open, setOpen] = useState(false);
const [isPending, setIsPending] = useState(false);
const request = useCallback((): void => setOpen(true), []);
const cancel = useCallback((): void => setOpen(false), []);
const confirm = useCallback(async (): Promise<void> => {
setIsPending(true);
try {
await action();
setOpen(false);
} finally {
setIsPending(false);
}
}, [action]);
return useMemo(
() => ({ open, isPending, request, confirm, cancel }),
[open, isPending, request, confirm, cancel],
);
}

View File

@@ -1,117 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
interface UseInlineOverflowCountOptions {
itemCount: number;
/** Horizontal gap between items, in px. */
gap?: number;
/** Width kept free at the end of the line for a trailing "+N" trigger, in px. */
reserveWidth?: number;
/** Pause measuring (e.g. while expanded) without unmounting. */
enabled?: boolean;
}
interface UseInlineOverflowCountResult {
containerRef: React.RefObject<HTMLDivElement>;
visibleCount: number;
overflowCount: number;
}
/**
* Measures how many of a container's children (each marked
* `data-overflow-item="true"`) fit on a single line, reserving `reserveWidth`
* for a trailing "+N" trigger. Item widths are cached, so children hidden with
* `display: none` still count toward the fit; measuring pauses while `enabled`
* is false.
*/
export function useInlineOverflowCount({
itemCount,
gap = 8,
reserveWidth = 0,
enabled = true,
}: UseInlineOverflowCountOptions): UseInlineOverflowCountResult {
const containerRef = useRef<HTMLDivElement>(null);
const [visibleCount, setVisibleCount] = useState(itemCount);
const itemWidthsRef = useRef<number[]>([]);
const enabledRef = useRef(enabled);
enabledRef.current = enabled;
useEffect(() => {
itemWidthsRef.current = [];
setVisibleCount(itemCount);
}, [itemCount]);
const measure = useCallback((): void => {
const container = containerRef.current;
if (!container || !enabledRef.current) {
return;
}
const itemElements = Array.from(container.children).filter(
(itemElement): itemElement is HTMLElement =>
itemElement instanceof HTMLElement &&
itemElement.dataset.overflowItem === 'true',
);
if (itemElements.length === 0) {
setVisibleCount(0);
return;
}
itemElements.forEach((itemElement, index) => {
if (itemElement.offsetWidth > 0) {
itemWidthsRef.current[index] = itemElement.offsetWidth;
}
});
const cachedWidths: number[] = [];
for (let index = 0; index < itemElements.length; index += 1) {
const cachedWidth = itemWidthsRef.current[index];
if (cachedWidth == null) {
// Width not cached yet — reveal everything for one frame so it gets
// measured, then the next pass collapses accurately.
setVisibleCount(itemElements.length);
return;
}
cachedWidths.push(cachedWidth);
}
const containerWidth = container.clientWidth;
const totalWidth = cachedWidths.reduce(
(runningTotal, itemWidth, index) =>
runningTotal + itemWidth + (index > 0 ? gap : 0),
0,
);
if (totalWidth <= containerWidth) {
setVisibleCount(itemElements.length);
return;
}
const availableWidth = containerWidth - reserveWidth;
let usedWidth = 0;
let fitCount = 0;
for (let index = 0; index < cachedWidths.length; index += 1) {
const itemWidthWithGap = cachedWidths[index] + (index > 0 ? gap : 0);
if (usedWidth + itemWidthWithGap > availableWidth && fitCount > 0) {
break;
}
usedWidth += itemWidthWithGap;
fitCount += 1;
}
setVisibleCount(Math.max(1, fitCount));
}, [gap, reserveWidth]);
useEffect(() => {
const container = containerRef.current;
if (!container) {
return undefined;
}
const observer = new ResizeObserver(() => measure());
observer.observe(container);
Array.from(container.children).forEach((child) => observer.observe(child));
measure();
return (): void => observer.disconnect();
}, [measure, itemCount, enabled]);
return {
containerRef,
visibleCount,
overflowCount: Math.max(0, itemCount - visibleCount),
};
}

View File

@@ -1,5 +1,3 @@
@use '../../../../styles/scrollbar' as *;
.legend-search-container {
flex-shrink: 0;
width: 100%;
@@ -17,10 +15,6 @@
gap: 12px;
height: 100%;
width: 100%;
// Allow the flex children to shrink below their content height so the
// virtualized grid scrolls within the capped legend height instead of
// overflowing the wrapper (default min-height:auto would block the shrink).
min-height: 0;
&:has(.legend-item-focused) .legend-item {
opacity: 0.3;
@@ -39,11 +33,6 @@
}
.legend-virtuoso-container {
// flex:1 + min-height:0 pins the scroller to the space left after the
// search box (RIGHT legend) and lets it scroll instead of growing to fit
// every row — without this the grid overflows a BOTTOM legend's fixed height.
flex: 1;
min-height: 0;
height: 100%;
width: 100%;
@@ -78,7 +67,18 @@
}
}
@include custom-scrollbar;
&::-webkit-scrollbar {
width: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--l3-background);
border-radius: 0.5rem;
}
}
}
@@ -108,10 +108,6 @@
align-items: center;
gap: 6px;
padding: 4px 8px;
// Include padding within the width so a full-width row (legend-item-right) fits its
// column instead of overflowing by the 16px horizontal padding — there is no global
// border-box reset, so the default content-box would make it overflow.
box-sizing: border-box;
max-width: 100%;
overflow: hidden;
border-radius: 4px;

View File

@@ -87,7 +87,7 @@ export class UPlotSeriesBuilder extends ConfigBuilder<
lineConfig.fill = `${finalFillColor}40`;
} else if (fillMode && fillMode !== FillMode.None) {
if (fillMode === FillMode.Solid) {
lineConfig.fill = `${finalFillColor}70`;
lineConfig.fill = finalFillColor;
} else if (fillMode === FillMode.Gradient) {
lineConfig.fill = (self: uPlot): CanvasGradient =>
generateGradientFill(self, finalFillColor, 'rgba(0, 0, 0, 0)');

View File

@@ -1,7 +1,11 @@
.dashboardActionsContainer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.dashboardActionsSecondary {
display: flex;
gap: 12px;
}

View File

@@ -1,42 +1,32 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { FullScreenHandle } from 'react-full-screen';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use';
import {
Braces,
ClipboardCopy,
Configure,
Copy,
Ellipsis,
FileJson,
Fullscreen,
Grid3X3,
LockKeyhole,
PenLine,
Plus,
SquareStack,
Trash2,
} from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
import { toast } from '@signozhq/ui/sonner';
import { cloneDashboardV2 } from 'api/generated/services/dashboard';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { USER_ROLES } from 'types/roles';
import ConfirmDeleteDialog from '../../components/ConfirmDeleteDialog/ConfirmDeleteDialog';
import DashboardSettings from '../../DashboardSettings';
import { useAddSection } from '../../PanelsAndSectionsLayout/Section/hooks/useAddSection';
import SectionTitleModal from '../../PanelsAndSectionsLayout/Section/SectionTitleModal';
import JsonEditorDrawer from '../JsonEditorDrawer/JsonEditorDrawer';
import SettingsDrawer from '../SettingsDrawer';
import styles from './DashboardActions.module.scss';
import { useDashboardStore } from '../../store/useDashboardStore';
@@ -65,31 +55,14 @@ function DashboardActions({
const canEdit = useDashboardStore((s) => s.isEditable);
const { user } = useAppContext();
const { t } = useTranslation(['dashboard', 'common']);
const { safeNavigate } = useSafeNavigate();
const { showErrorModal } = useErrorModal();
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] =
useState<boolean>(false);
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState<boolean>(false);
const [isCloning, setIsCloning] = useState<boolean>(false);
const [isNewSectionOpen, setIsNewSectionOpen] = useState<boolean>(false);
const [state, setCopy] = useCopyToClipboard();
const [isDeleteOpen, setIsDeleteOpen] = useState<boolean>(false);
const deleteDashboardMutation = useDeleteDashboard(dashboard.id);
const { addSection, isSaving: isAddingSection } = useAddSection({
layouts: dashboard.spec.layouts,
});
const handleCreateSection = useCallback(
async (title: string): Promise<void> => {
await addSection(title);
setIsNewSectionOpen(false);
},
[addSection],
);
useEffect(() => {
if (state.error) {
toast.error(t('something_went_wrong', { ns: 'common' }));
@@ -116,24 +89,6 @@ function DashboardActions({
URL.revokeObjectURL(url);
}, [dashboardDataJSON, title]);
const handleClone = useCallback(async (): Promise<void> => {
if (!dashboard.id) {
return;
}
try {
setIsCloning(true);
const response = await cloneDashboardV2({ id: dashboard.id });
toast.success('Dashboard cloned');
safeNavigate(
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id }),
);
} catch (error) {
showErrorModal(error as APIError);
} finally {
setIsCloning(false);
}
}, [dashboard.id, safeNavigate, showErrorModal]);
const handleConfirmDelete = useCallback((): void => {
deleteDashboardMutation.mutate(undefined, {
onSuccess: () => {
@@ -144,24 +99,17 @@ function DashboardActions({
}, [deleteDashboardMutation]);
const menuItems = useMemo<MenuItem[]>(() => {
const dashboardGroup: MenuItem[] = [];
const editGroup: MenuItem[] = [];
if (canEdit) {
dashboardGroup.push({
editGroup.push({
key: 'rename',
label: 'Rename',
icon: <PenLine size={14} />,
onClick: onOpenRename,
});
}
dashboardGroup.push({
key: 'clone',
label: 'Clone dashboard',
icon: <Copy size={14} />,
disabled: isCloning,
onClick: (): void => void handleClone(),
});
if (isAuthor || user.role === USER_ROLES.ADMIN) {
dashboardGroup.push({
editGroup.push({
key: 'lock',
label: isDashboardLocked ? 'Unlock dashboard' : 'Lock dashboard',
icon: <LockKeyhole size={14} />,
@@ -169,14 +117,14 @@ function DashboardActions({
onClick: onLockToggle,
});
}
dashboardGroup.push({
editGroup.push({
key: 'fullscreen',
label: 'Full screen',
icon: <Fullscreen size={14} />,
onClick: handle.enter,
});
const dataGroup: MenuItem[] = [
const exportGroup: MenuItem[] = [
{
key: 'export',
label: 'Export JSON',
@@ -191,35 +139,7 @@ function DashboardActions({
},
];
const layoutGroup: MenuItem[] = [];
if (canEdit) {
layoutGroup.push({
key: 'new-section',
label: 'New section',
icon: <SquareStack size={14} />,
onClick: (): void => setIsNewSectionOpen(true),
});
}
const items: MenuItem[] = [
{
type: 'group',
key: 'group-dashboard',
label: 'Dashboard',
children: dashboardGroup,
},
{ type: 'group', key: 'group-data', label: 'Data', children: dataGroup },
];
if (layoutGroup.length > 0) {
items.push({
type: 'group',
key: 'group-layout',
label: 'Layout',
children: layoutGroup,
});
}
items.push(
{ type: 'divider', key: 'divider-danger' },
const dangerGroup: MenuItem[] = [
{
key: 'delete',
label: 'Delete dashboard',
@@ -227,85 +147,74 @@ function DashboardActions({
danger: true,
onClick: (): void => setIsDeleteOpen(true),
},
);
return items;
];
return [editGroup, exportGroup, dangerGroup]
.filter((group) => group.length > 0)
.flatMap((group, index) =>
index > 0 ? [{ type: 'divider' } as MenuItem, ...group] : group,
);
}, [
canEdit,
isCloning,
isDashboardLocked,
isAuthor,
user.role,
isDashboardLocked,
dashboard.createdBy,
onOpenRename,
handleClone,
onLockToggle,
handle.enter,
exportJSON,
setCopy,
dashboardDataJSON,
canEdit,
]);
return (
<div className={styles.dashboardActionsContainer}>
<DropdownMenuSimple menu={{ items: menuItems }}>
<Button
variant="solid"
color="secondary"
size="md"
prefix={<Grid3X3 size="md" />}
testId="options"
>
Actions
</Button>
</DropdownMenuSimple>
{canEdit && (
<>
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
<div className={styles.dashboardActionsSecondary}>
<DropdownMenuSimple menu={{ items: menuItems }}>
<Button
variant="solid"
color="secondary"
prefix={<Configure size="md" />}
testId="show-drawer"
onClick={(): void => setIsSettingsDrawerOpen(true)}
size="icon"
prefix={<Ellipsis size="md" />}
testId="options"
/>
</DropdownMenuSimple>
{canEdit && (
<>
<Button
variant="solid"
color="secondary"
prefix={<Configure size="md" />}
testId="show-drawer"
onClick={(): void => setIsSettingsDrawerOpen(true)}
size="md"
>
Configure
</Button>
<SettingsDrawer
drawerTitle="Dashboard Configuration"
isOpen={isSettingsDrawerOpen}
onClose={(): void => setIsSettingsDrawerOpen(false)}
>
<DashboardSettings dashboard={dashboard} />
</SettingsDrawer>
</>
)}
{!isDashboardLocked && (
<Button
variant="solid"
color="primary"
onClick={onAddPanel}
prefix={<Plus size="md" />}
testId="add-panel-header"
size="md"
>
Configure
New Panel
</Button>
<SettingsDrawer
drawerTitle="Dashboard Configuration"
isOpen={isSettingsDrawerOpen}
onClose={(): void => setIsSettingsDrawerOpen(false)}
>
<DashboardSettings dashboard={dashboard} />
</SettingsDrawer>
</>
)}
<Button
variant="solid"
color="secondary"
prefix={<Braces size="md" />}
testId="edit-json"
onClick={(): void => setIsJsonEditorOpen(true)}
size="md"
>
Edit as JSON
</Button>
{!isDashboardLocked && (
<Button
variant="solid"
color="primary"
onClick={onAddPanel}
prefix={<Plus size="md" />}
testId="add-panel-header"
size="md"
>
New Panel
</Button>
)}
<JsonEditorDrawer
dashboard={dashboard}
isOpen={isJsonEditorOpen}
onClose={(): void => setIsJsonEditorOpen(false)}
/>
)}
</div>
<ConfirmDeleteDialog
open={isDeleteOpen}
title={`Delete dashboard"?`}
@@ -314,15 +223,6 @@ function DashboardActions({
onConfirm={handleConfirmDelete}
onClose={(): void => setIsDeleteOpen(false)}
/>
<SectionTitleModal
open={isNewSectionOpen}
heading="New section"
okText="Create section"
initialValue=""
isSaving={isAddingSection}
onClose={(): void => setIsNewSectionOpen(false)}
onSubmit={handleCreateSection}
/>
</div>
);
}

View File

@@ -1,9 +1,19 @@
.dashboardInfo {
display: flex;
flex: 1;
flex-direction: column;
gap: 8px;
width: 40%;
@media (min-width: 1280px) {
width: 30%;
}
}
.dashboardTitleContainer {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
width: 100%;
}
.dashboardImage {
@@ -11,8 +21,9 @@
}
.dashboardTitle {
flex: 0 1 auto;
flex: 1;
min-width: 0;
max-width: fit-content;
color: var(--l1-foreground);
font-size: 18px;
font-weight: 500;
@@ -26,19 +37,6 @@
cursor: text !important;
}
.descriptionIcon {
flex-shrink: 0;
color: var(--l2-foreground);
cursor: help;
}
.divider {
flex-shrink: 0;
width: 1px;
height: 18px;
background: var(--l2-border);
}
.dashboardTitleEditor {
display: flex;
align-items: center;
@@ -56,13 +54,8 @@
flex-shrink: 0;
}
/* Flexes into the remaining space and clips so the ResizeObserver can measure
how many tags fit before collapsing the rest into a `+N` badge. */
.dashboardTags {
display: flex;
flex: 1 1 0;
align-items: center;
gap: 4px;
min-width: 0;
overflow: hidden;
flex-wrap: wrap;
gap: 8px;
}

View File

@@ -1,5 +1,5 @@
import { KeyboardEvent } from 'react';
import { Check, Globe, LockKeyhole, SolidInfoCircle, X } from '@signozhq/icons';
import { Check, Globe, LockKeyhole, X } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
@@ -9,7 +9,6 @@ import cx from 'classnames';
import { isEmpty } from 'lodash-es';
import styles from './DashboardInfo.module.scss';
import { useVisibleTagCount } from './useVisibleTagCount';
import { useDashboardStore } from '../../store/useDashboardStore';
interface DashboardInfoProps {
@@ -46,11 +45,6 @@ function DashboardInfo({
const hasTags = tags.length > 0;
const hasDescription = !isEmpty(description);
const { containerRef, visibleCount } = useVisibleTagCount(tags);
const needsOverflow = tags.length > visibleCount;
const visibleTags = needsOverflow ? tags.slice(0, visibleCount) : tags;
const remainingTags = needsOverflow ? tags.slice(visibleCount) : [];
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
if (event.key === 'Enter') {
event.preventDefault();
@@ -62,106 +56,83 @@ function DashboardInfo({
return (
<div className={styles.dashboardInfo}>
<img src={image} alt={title} className={styles.dashboardImage} />
<div className={styles.dashboardTitleContainer}>
<img src={image} alt={title} className={styles.dashboardImage} />
{isEditing ? (
<div className={styles.dashboardTitleEditor}>
<Input
autoFocus
value={draft}
testId="dashboard-title-input"
maxLength={120}
className={styles.dashboardTitleInput}
onChange={(e): void => onDraftChange(e.target.value)}
onKeyDown={onKeyDown}
/>
<Button
type="button"
variant="outlined"
color="primary"
size="icon"
className={styles.dashboardTitleActionButton}
aria-label="Save title"
testId="dashboard-title-save"
onClick={onCommit}
>
<Check size={14} />
</Button>
<Button
type="button"
variant="outlined"
color="secondary"
size="icon"
className={styles.dashboardTitleActionButton}
aria-label="Cancel title edit"
testId="dashboard-title-cancel"
onClick={onCancel}
>
<X size={14} />
</Button>
</div>
) : (
<TooltipSimple title={title}>
<Typography.Text
className={cx(styles.dashboardTitle, {
[styles.dashboardTitleHover]: canEdit,
})}
data-testid="dashboard-title"
onClick={canEdit ? onStartEdit : undefined}
>
{title}
</Typography.Text>
</TooltipSimple>
)}
{isEditing ? (
<div className={styles.dashboardTitleEditor}>
<Input
autoFocus
value={draft}
testId="dashboard-title-input"
maxLength={120}
className={styles.dashboardTitleInput}
onChange={(e): void => onDraftChange(e.target.value)}
onKeyDown={onKeyDown}
/>
<Button
type="button"
variant="outlined"
color="primary"
size="icon"
className={styles.dashboardTitleActionButton}
aria-label="Save title"
testId="dashboard-title-save"
onClick={onCommit}
>
<Check size={14} />
</Button>
<Button
type="button"
variant="outlined"
color="secondary"
size="icon"
className={styles.dashboardTitleActionButton}
aria-label="Cancel title edit"
testId="dashboard-title-cancel"
onClick={onCancel}
>
<X size={14} />
</Button>
{isPublicDashboard && (
<TooltipSimple title="This dashboard is publicly accessible">
<Globe size={14} />
</TooltipSimple>
)}
{isDashboardLocked && (
<TooltipSimple title="This dashboard is locked">
<LockKeyhole size={14} />
</TooltipSimple>
)}
</div>
{hasTags && (
<div className={styles.dashboardTags}>
{tags.map((tag) => (
<Badge key={tag} color="warning" variant="outline">
{tag}
</Badge>
))}
</div>
) : (
<TooltipSimple title={title}>
<Typography.Text
className={cx(styles.dashboardTitle, {
[styles.dashboardTitleHover]: canEdit,
})}
data-testid="dashboard-title"
onClick={canEdit ? onStartEdit : undefined}
>
{title}
</Typography.Text>
</TooltipSimple>
)}
{hasDescription && (
<TooltipSimple title={description}>
<SolidInfoCircle
className={styles.descriptionIcon}
size={14}
data-testid="dashboard-description-info"
/>
</TooltipSimple>
)}
{isPublicDashboard && (
<TooltipSimple title="This dashboard is publicly accessible">
<Globe size={14} />
</TooltipSimple>
)}
{isDashboardLocked && (
<TooltipSimple title="This dashboard is locked">
<LockKeyhole size={14} />
</TooltipSimple>
)}
{hasTags && (
<>
<span className={styles.divider} />
<div
ref={containerRef}
className={styles.dashboardTags}
data-testid="dashboard-tags"
>
{visibleTags.map((tag) => (
<Badge key={tag} color="warning" variant="outline">
{tag}
</Badge>
))}
{remainingTags.length > 0 && (
<TooltipSimple title={remainingTags.join(', ')}>
<Badge
color="warning"
variant="outline"
data-testid="dashboard-tags-overflow"
>
+{remainingTags.length}
</Badge>
</TooltipSimple>
)}
</div>
</>
<Typography.Text color="muted">{description}</Typography.Text>
)}
</div>
);

View File

@@ -1,62 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import {
BADGE_GAP,
estimateBadgeWidth,
OVERFLOW_BADGE_WIDTH,
} from 'components/Alerts/LabelColumn/utils';
interface Result {
containerRef: React.RefObject<HTMLDivElement>;
visibleCount: number;
}
/**
* Measures how many tags fit in the container and returns the visible count,
* reserving room for the `+N` overflow badge. Reuses the badge-width estimation
* from the alerts LabelColumn so dashboards and alerts overflow identically.
*/
export function useVisibleTagCount(tags: string[]): Result {
const containerRef = useRef<HTMLDivElement>(null);
const [visibleCount, setVisibleCount] = useState(tags.length);
const calculateVisible = useCallback(
(width: number): number => {
if (width <= 0) {
return 1;
}
const availableWidth = width - OVERFLOW_BADGE_WIDTH - BADGE_GAP;
let usedWidth = 0;
let count = 0;
for (const tag of tags) {
const badgeWidth = estimateBadgeWidth(tag) + BADGE_GAP;
if (usedWidth + badgeWidth > availableWidth && count > 0) {
break;
}
usedWidth += badgeWidth;
count += 1;
}
return Math.max(1, count);
},
[tags],
);
useEffect(() => {
const container = containerRef.current;
if (!container) {
return undefined;
}
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
if (entry && entry.contentRect.width > 0) {
setVisibleCount(calculateVisible(entry.contentRect.width));
}
});
observer.observe(container);
if (container.clientWidth > 0) {
setVisibleCount(calculateVisible(container.clientWidth));
}
return (): void => observer.disconnect();
}, [calculateVisible]);
return { containerRef, visibleCount };
}

View File

@@ -5,9 +5,7 @@
color: var(--l2-foreground);
background-color: var(--l1-background);
padding: 16px;
box-shadow:
0 1px 0 0 var(--l2-border),
0 6px 12px -10px var(--l2-border);
box-shadow: 0 2px 2px 0px var(--l2-border);
}
.dashboardPageToolbarSubContainer {
@@ -18,22 +16,5 @@
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
width: 100%;
}
.toolbarRow2 {
width: 100%;
margin-top: 12px;
&::after {
display: block;
clear: both;
content: '';
}
}
.timeCluster {
float: right;
margin: 0 0 0 16px;
}

View File

@@ -1,72 +0,0 @@
.root {
:global(.ant-drawer-wrapper-body) {
border-left: 1px solid var(--l1-border);
background: var(--l2-background);
}
:global(.ant-drawer-header) {
height: 48px;
border-bottom: 1px solid var(--l1-border);
:global(.ant-drawer-title) {
color: var(--l2-foreground);
font-family: Inter;
font-size: 14px;
font-weight: 500;
}
}
:global(.ant-drawer-body) {
padding: 0;
display: flex;
min-height: 0;
}
:global(.ant-drawer-footer) {
padding: 12px 16px;
border-top: 1px solid var(--l1-border);
}
}
.body {
display: flex;
flex: 1;
min-width: 0;
min-height: 0;
flex-direction: column;
}
.editor {
flex: 1;
min-height: 0;
}
.footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.validation {
min-width: 0;
overflow: hidden;
font-family: 'Space Mono', monospace;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.validationValid {
color: var(--bg-forest-400);
}
.validationInvalid {
color: var(--bg-cherry-400);
}
.footerActions {
display: flex;
flex-shrink: 0;
gap: 8px;
}

View File

@@ -1,141 +0,0 @@
import { KeyboardEvent, useCallback } from 'react';
import MEditor from '@monaco-editor/react';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { Drawer } from 'antd';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import { useCopyToClipboard } from 'react-use';
import { toast } from '@signozhq/ui/sonner';
import { defineJsonEditorTheme, JSON_EDITOR_THEME } from './editorTheme';
import styles from './JsonEditorDrawer.module.scss';
import JsonEditorToolbar from './JsonEditorToolbar';
import { useJsonEditor } from './useJsonEditor';
interface JsonEditorDrawerProps {
dashboard: DashboardtypesGettableDashboardV2DTO;
isOpen: boolean;
onClose: () => void;
}
function JsonEditorDrawer({
dashboard,
isOpen,
onClose,
}: JsonEditorDrawerProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const { draft, setDraft, validity, isDirty, isSaving, format, reset, apply } =
useJsonEditor({ dashboard, isOpen, onApplied: onClose });
const onCopy = useCallback((): void => {
copyToClipboard(draft);
toast.success('JSON copied to clipboard');
}, [copyToClipboard, draft]);
const onDownload = useCallback((): void => {
const blob = new Blob([draft], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${dashboard.name || 'dashboard'}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, [draft, dashboard.name]);
const onKeyDown = useCallback(
(event: KeyboardEvent<HTMLDivElement>): void => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
event.preventDefault();
void apply();
}
},
[apply],
);
const applyDisabled = !isDirty || !validity.valid || isSaving;
const validationText = validity.valid
? `Valid JSON · ${validity.lineCount} lines`
: `Line ${validity.errorLine ?? '?'} · ${validity.message ?? 'Invalid JSON'}`;
return (
<Drawer
title="Dashboard JSON"
placement="right"
width={660}
onClose={onClose}
open={isOpen}
rootClassName={styles.root}
footer={
<div className={styles.footer}>
<Typography.Text
className={cx(styles.validation, {
[styles.validationValid]: validity.valid,
[styles.validationInvalid]: !validity.valid,
})}
data-testid="json-editor-validation"
>
{validationText}
</Typography.Text>
<div className={styles.footerActions}>
<Button
variant="outlined"
color="secondary"
size="md"
testId="json-editor-cancel"
onClick={onClose}
>
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="md"
testId="json-editor-apply"
disabled={applyDisabled}
onClick={(): void => void apply()}
>
Apply changes
</Button>
</div>
</div>
}
>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div className={styles.body} onKeyDown={onKeyDown}>
<JsonEditorToolbar
isDirty={isDirty}
onFormat={format}
onCopy={onCopy}
onDownload={onDownload}
onReset={reset}
/>
<div className={styles.editor}>
<MEditor
language="json"
height="100%"
value={draft}
onChange={(value): void => setDraft(value ?? '')}
options={{
scrollbar: { alwaysConsumeMouseWheel: false },
minimap: { enabled: false },
fontSize: 13,
fontFamily: 'Space Mono',
}}
theme="vs-dark"
onMount={(editor, monaco): void => {
defineJsonEditorTheme(monaco, editor.getContainerDomNode());
monaco.editor.setTheme(JSON_EDITOR_THEME);
void document.fonts.ready.then(() => monaco.editor.remeasureFonts());
}}
/>
</div>
</div>
</Drawer>
);
}
export default JsonEditorDrawer;

View File

@@ -1,12 +0,0 @@
.toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
border-bottom: 1px solid var(--l1-border);
background: var(--l1-background);
}
.spacer {
flex: 1;
}

View File

@@ -1,69 +0,0 @@
import { AlignLeft, Copy, Download, RotateCcw } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import styles from './JsonEditorToolbar.module.scss';
interface JsonEditorToolbarProps {
isDirty: boolean;
onFormat: () => void;
onCopy: () => void;
onDownload: () => void;
onReset: () => void;
}
function JsonEditorToolbar({
isDirty,
onFormat,
onCopy,
onDownload,
onReset,
}: JsonEditorToolbarProps): JSX.Element {
return (
<div className={styles.toolbar}>
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={<AlignLeft size={14} />}
testId="json-editor-format"
onClick={onFormat}
>
Format
</Button>
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={<Copy size={14} />}
testId="json-editor-copy"
onClick={onCopy}
>
Copy
</Button>
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={<Download size={14} />}
testId="json-editor-download"
onClick={onDownload}
>
Download
</Button>
<div className={styles.spacer} />
<Button
variant="ghost"
color="secondary"
size="sm"
prefix={<RotateCcw size={14} />}
testId="json-editor-reset"
disabled={!isDirty}
onClick={onReset}
>
Reset
</Button>
</div>
);
}
export default JsonEditorToolbar;

View File

@@ -1,165 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import JsonEditorDrawer from '../JsonEditorDrawer';
import { useJsonEditor } from '../useJsonEditor';
jest.mock('../useJsonEditor', () => ({ useJsonEditor: jest.fn() }));
jest.mock('@monaco-editor/react', () => ({
__esModule: true,
default: ({
value,
onChange,
}: {
value: string;
onChange: (next?: string) => void;
}): JSX.Element => (
<textarea
aria-label="json-editor"
data-testid="monaco"
value={value}
onChange={(e): void => onChange(e.target.value)}
/>
),
}));
jest.mock('@signozhq/ui/sonner', () => ({
toast: { success: jest.fn(), error: jest.fn() },
}));
jest.mock('react-use', () => ({
useCopyToClipboard: (): [unknown, jest.Mock] => [{}, jest.fn()],
}));
const mockUseJsonEditor = useJsonEditor as jest.Mock;
const dashboard = {
id: 'dash-1',
name: 'My dashboard',
} as unknown as DashboardtypesGettableDashboardV2DTO;
function hookValue(
overrides: Partial<ReturnType<typeof useJsonEditor>> = {},
): ReturnType<typeof useJsonEditor> {
return {
draft: '{\n "a": 1\n}',
setDraft: jest.fn(),
validity: { valid: true, lineCount: 3 },
isDirty: true,
isSaving: false,
format: jest.fn(),
reset: jest.fn(),
apply: jest.fn().mockResolvedValue(undefined),
...overrides,
};
}
describe('JsonEditorDrawer', () => {
beforeEach(() => jest.clearAllMocks());
it('renders the toolbar, editor and footer actions when open', () => {
mockUseJsonEditor.mockReturnValue(hookValue());
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
expect(screen.getByTestId('json-editor-format')).toBeInTheDocument();
expect(screen.getByTestId('json-editor-copy')).toBeInTheDocument();
expect(screen.getByTestId('json-editor-download')).toBeInTheDocument();
expect(screen.getByTestId('json-editor-reset')).toBeInTheDocument();
expect(screen.getByTestId('json-editor-apply')).toBeInTheDocument();
expect(screen.getByTestId('monaco')).toBeInTheDocument();
});
it('shows a valid status with the line count', () => {
mockUseJsonEditor.mockReturnValue(
hookValue({ validity: { valid: true, lineCount: 12 } }),
);
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
expect(screen.getByTestId('json-editor-validation')).toHaveTextContent(
'Valid JSON · 12 lines',
);
});
it('shows the error line and message when invalid', () => {
mockUseJsonEditor.mockReturnValue(
hookValue({
validity: {
valid: false,
lineCount: 4,
errorLine: 3,
message: 'Unexpected token',
},
}),
);
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
expect(screen.getByTestId('json-editor-validation')).toHaveTextContent(
'Line 3 · Unexpected token',
);
});
it('disables Apply when not dirty, invalid, or saving', () => {
mockUseJsonEditor.mockReturnValue(hookValue({ isDirty: false }));
const { rerender } = render(
<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />,
);
expect(screen.getByTestId('json-editor-apply')).toBeDisabled();
mockUseJsonEditor.mockReturnValue(
hookValue({ validity: { valid: false, lineCount: 1 } }),
);
rerender(
<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />,
);
expect(screen.getByTestId('json-editor-apply')).toBeDisabled();
mockUseJsonEditor.mockReturnValue(hookValue({ isSaving: true }));
rerender(
<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />,
);
expect(screen.getByTestId('json-editor-apply')).toBeDisabled();
});
it('wires toolbar and footer buttons to the hook callbacks', () => {
const value = hookValue();
mockUseJsonEditor.mockReturnValue(value);
const onClose = jest.fn();
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={onClose} />);
fireEvent.click(screen.getByTestId('json-editor-format'));
expect(value.format).toHaveBeenCalled();
fireEvent.click(screen.getByTestId('json-editor-reset'));
expect(value.reset).toHaveBeenCalled();
fireEvent.click(screen.getByTestId('json-editor-apply'));
expect(value.apply).toHaveBeenCalled();
fireEvent.click(screen.getByTestId('json-editor-cancel'));
expect(onClose).toHaveBeenCalled();
});
it('forwards editor changes to setDraft', () => {
const value = hookValue();
mockUseJsonEditor.mockReturnValue(value);
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
fireEvent.change(screen.getByTestId('monaco'), {
target: { value: '{"b":2}' },
});
expect(value.setDraft).toHaveBeenCalledWith('{"b":2}');
});
it('applies on Cmd/Ctrl+Enter', () => {
const value = hookValue();
mockUseJsonEditor.mockReturnValue(value);
render(<JsonEditorDrawer dashboard={dashboard} isOpen onClose={jest.fn()} />);
fireEvent.keyDown(screen.getByTestId('monaco'), {
key: 'Enter',
metaKey: true,
});
expect(value.apply).toHaveBeenCalled();
});
});

View File

@@ -1,179 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { updateDashboardV2 } from 'api/generated/services/dashboard';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import { toast } from '@signozhq/ui/sonner';
import { useJsonEditor } from '../useJsonEditor';
const mockRefetch = jest.fn();
const mockShowErrorModal = jest.fn();
jest.mock('../../../store/useDashboardStore', () => ({
useDashboardStore: (selector: (state: unknown) => unknown): unknown =>
selector({ dashboardId: 'dash-1', refetch: mockRefetch }),
}));
jest.mock('providers/ErrorModalProvider', () => ({
useErrorModal: (): { showErrorModal: jest.Mock } => ({
showErrorModal: mockShowErrorModal,
}),
}));
jest.mock('api/generated/services/dashboard', () => ({
updateDashboardV2: jest.fn(),
}));
jest.mock('@signozhq/ui/sonner', () => ({
toast: { success: jest.fn(), error: jest.fn() },
}));
const mockUpdate = updateDashboardV2 as jest.Mock;
const mockToastSuccess = toast.success as jest.Mock;
const dashboard = {
id: 'dash-1',
name: 'My dashboard',
schemaVersion: 'v6',
image: 'icon.png',
tags: [{ key: 'env', value: 'prod' }],
spec: {
display: { name: 'My dashboard' },
panels: {},
layouts: [],
variables: [],
},
} as unknown as DashboardtypesGettableDashboardV2DTO;
const serialized = JSON.stringify(dashboard, null, 2);
describe('useJsonEditor', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUpdate.mockResolvedValue({});
});
it('seeds the draft from the dashboard and reports valid, non-dirty state', () => {
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
);
expect(result.current.draft).toBe(serialized);
expect(result.current.isDirty).toBe(false);
expect(result.current.validity.valid).toBe(true);
expect(result.current.validity.lineCount).toBe(serialized.split('\n').length);
});
it('flags invalid JSON with a line number and marks the draft dirty', () => {
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
);
act(() => result.current.setDraft('{\n "name": ,\n}'));
expect(result.current.validity.valid).toBe(false);
expect(result.current.validity.message).toBeDefined();
expect(result.current.isDirty).toBe(true);
});
it('format() pretty-prints valid JSON and leaves invalid JSON untouched', () => {
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
);
act(() => result.current.setDraft('{"a":1}'));
act(() => result.current.format());
expect(result.current.draft).toBe('{\n "a": 1\n}');
act(() => result.current.setDraft('{bad'));
act(() => result.current.format());
expect(result.current.draft).toBe('{bad');
});
it('reset() restores the last-applied text', () => {
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
);
act(() => result.current.setDraft('edited'));
expect(result.current.isDirty).toBe(true);
act(() => result.current.reset());
expect(result.current.draft).toBe(serialized);
expect(result.current.isDirty).toBe(false);
});
it('apply() is a no-op when the draft is unchanged or invalid', async () => {
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
);
await act(async () => {
await result.current.apply();
});
expect(mockUpdate).not.toHaveBeenCalled();
act(() => result.current.setDraft('{bad'));
await act(async () => {
await result.current.apply();
});
expect(mockUpdate).not.toHaveBeenCalled();
});
it('apply() PUTs the narrowed body, toasts, refetches and calls onApplied', async () => {
const onApplied = jest.fn();
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied }),
);
const next = { ...dashboard, name: 'Renamed' };
act(() => result.current.setDraft(JSON.stringify(next)));
await act(async () => {
await result.current.apply();
});
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(mockUpdate).toHaveBeenCalledWith(
{ id: 'dash-1' },
expect.objectContaining({
name: 'Renamed',
schemaVersion: 'v6',
spec: next.spec,
tags: next.tags,
}),
);
expect(mockToastSuccess).toHaveBeenCalled();
expect(mockRefetch).toHaveBeenCalled();
expect(onApplied).toHaveBeenCalled();
});
it('apply() surfaces errors through the error modal', async () => {
mockUpdate.mockRejectedValueOnce(new Error('boom'));
const { result } = renderHook(() =>
useJsonEditor({ dashboard, isOpen: true, onApplied: jest.fn() }),
);
act(() =>
result.current.setDraft(JSON.stringify({ ...dashboard, name: 'X' })),
);
await act(async () => {
await result.current.apply();
});
expect(mockShowErrorModal).toHaveBeenCalled();
});
it('re-seeds the draft when the drawer re-opens', () => {
const onApplied = jest.fn();
const { result, rerender } = renderHook(
(props: { isOpen: boolean }) =>
useJsonEditor({ dashboard, isOpen: props.isOpen, onApplied }),
{ initialProps: { isOpen: false } },
);
act(() => result.current.setDraft('stale edit'));
expect(result.current.draft).toBe('stale edit');
rerender({ isOpen: true });
expect(result.current.draft).toBe(serialized);
});
});

View File

@@ -1,26 +0,0 @@
import type {
DashboardtypesGettableDashboardV2DTO,
DashboardtypesDashboardSpecDTO,
DashboardtypesUpdatableDashboardV2DTO,
TagtypesPostableTagDTO,
} from 'api/generated/services/sigNoz.schemas';
/**
* Narrow a parsed (full Gettable-shaped) dashboard JSON down to the PUT-updatable
* body. The editor shows the whole dashboard for readability, but the update
* endpoint only accepts `{ name, schemaVersion, image, tags, spec }` — the
* server owns `id`, `locked`, timestamps, etc., so we drop them here.
*/
export function dashboardToUpdatable(
parsed: Record<string, unknown>,
): DashboardtypesUpdatableDashboardV2DTO {
const dashboard = parsed as Partial<DashboardtypesGettableDashboardV2DTO>;
return {
name: dashboard.name ?? '',
schemaVersion: dashboard.schemaVersion ?? 'v6',
image: dashboard.image,
tags: (dashboard.tags as TagtypesPostableTagDTO[] | null | undefined) ?? null,
spec: dashboard.spec as DashboardtypesDashboardSpecDTO,
};
}

View File

@@ -1,47 +0,0 @@
import type { Monaco } from '@monaco-editor/react';
export const JSON_EDITOR_THEME = 'signoz-json';
function token(el: HTMLElement, name: string): string {
return getComputedStyle(el).getPropertyValue(name).trim().replace('#', '');
}
function isDark(hex: string): boolean {
if (hex.length < 6) {
return true;
}
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return 0.299 * r + 0.587 * g + 0.114 * b < 128;
}
export function defineJsonEditorTheme(monaco: Monaco, el: HTMLElement): void {
const background = token(el, '--l1-background');
const foreground = token(el, '--l1-foreground');
const keyColor = token(el, '--bg-vanilla-400');
const valueColor = token(el, '--bg-robin-400');
const rules: { token: string; foreground: string }[] = [];
if (keyColor) {
rules.push({ token: 'string.key.json', foreground: keyColor });
}
if (valueColor) {
rules.push({ token: 'string.value.json', foreground: valueColor });
}
const colors: Record<string, string> = {};
if (background) {
colors['editor.background'] = `#${background}`;
}
if (foreground) {
colors['editor.foreground'] = `#${foreground}`;
}
monaco.editor.defineTheme(JSON_EDITOR_THEME, {
base: isDark(background) ? 'vs-dark' : 'vs',
inherit: true,
rules,
colors,
});
}

View File

@@ -1,148 +0,0 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { toast } from '@signozhq/ui/sonner';
import { updateDashboardV2 } from 'api/generated/services/dashboard';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { dashboardToUpdatable } from './dashboardToUpdatable';
import { useDashboardStore } from '../../store/useDashboardStore';
export interface JsonValidity {
valid: boolean;
lineCount: number;
/** 1-based line of the parse error, when known. */
errorLine?: number;
message?: string;
}
interface Params {
dashboard: DashboardtypesGettableDashboardV2DTO;
isOpen: boolean;
onApplied: () => void;
}
interface Result {
draft: string;
setDraft: (next: string) => void;
validity: JsonValidity;
isDirty: boolean;
isSaving: boolean;
format: () => void;
reset: () => void;
apply: () => Promise<void>;
}
const serialize = (dashboard: DashboardtypesGettableDashboardV2DTO): string =>
JSON.stringify(dashboard, null, 2);
/** Derive a 1-based line number from a `JSON.parse` "position N" error message. */
function errorLineFromMessage(
source: string,
message: string,
): number | undefined {
const match = /position (\d+)/.exec(message);
if (!match) {
return undefined;
}
const position = Number(match[1]);
return source.slice(0, position).split('\n').length;
}
/**
* Editor state for the dashboard JSON drawer: tracks the editable `draft`
* against the last-applied text, exposes live validation, and applies changes
* via the full-document update endpoint.
*/
export function useJsonEditor({
dashboard,
isOpen,
onApplied,
}: Params): Result {
const dashboardId = useDashboardStore((s) => s.dashboardId);
const refetch = useDashboardStore((s) => s.refetch);
const { showErrorModal } = useErrorModal();
const [appliedText, setAppliedText] = useState<string>(() =>
serialize(dashboard),
);
const [draft, setDraft] = useState<string>(appliedText);
const [isSaving, setIsSaving] = useState(false);
// Re-seed the editor from the current dashboard each time the drawer opens so
// it always reflects the latest persisted state (e.g. after a refetch).
useEffect(() => {
if (isOpen) {
const next = serialize(dashboard);
setAppliedText(next);
setDraft(next);
}
}, [isOpen, dashboard]);
const validity = useMemo<JsonValidity>(() => {
const lineCount = draft.split('\n').length;
try {
JSON.parse(draft);
return { valid: true, lineCount };
} catch (error) {
const message = error instanceof Error ? error.message : 'Invalid JSON';
return {
valid: false,
lineCount,
errorLine: errorLineFromMessage(draft, message),
message,
};
}
}, [draft]);
const isDirty = draft !== appliedText;
const format = useCallback((): void => {
try {
setDraft(JSON.stringify(JSON.parse(draft), null, 2));
} catch {
// Leave the draft untouched when it can't be parsed.
}
}, [draft]);
const reset = useCallback((): void => {
setDraft(appliedText);
}, [appliedText]);
const apply = useCallback(async (): Promise<void> => {
if (!validity.valid || !isDirty) {
return;
}
try {
setIsSaving(true);
const parsed = JSON.parse(draft) as Record<string, unknown>;
await updateDashboardV2({ id: dashboardId }, dashboardToUpdatable(parsed));
toast.success('Dashboard updated');
refetch();
onApplied();
} catch (error) {
showErrorModal(error as APIError);
} finally {
setIsSaving(false);
}
}, [
dashboardId,
validity.valid,
isDirty,
draft,
refetch,
onApplied,
showErrorModal,
]);
return {
draft,
setDraft,
validity,
isDirty,
isSaving,
format,
reset,
apply,
};
}

View File

@@ -12,7 +12,6 @@ import type {
DashboardtypesJSONPatchOperationDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useAppContext } from 'providers/App/App';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { useErrorModal } from 'providers/ErrorModalProvider';
@@ -140,15 +139,7 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
/>
</div>
{/* Row 2: the time selector floats top-right (declared first so the
variables bar's content wraps around it); the variables bar
collapses to one line and, when expanded, wraps full-width under it. */}
<div className={styles.toolbarRow2}>
<div className={styles.timeCluster}>
<DateTimeSelectionV2 showAutoRefresh hideShareModal />
</div>
<VariablesBar dashboard={dashboard} />
</div>
<VariablesBar dashboard={dashboard} />
</section>
);
}

View File

@@ -1,34 +1,24 @@
import { useEffect, useMemo, useState } from 'react';
import { Info } from '@signozhq/icons';
import { SelectSimple } from '@signozhq/ui/select';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
// eslint-disable-next-line signoz/no-antd-components -- fixed-option signal picker
// eslint-disable-next-line signoz/no-antd-components -- searchable async select: no @signozhq/ui equivalent
import { Select } from 'antd';
import { CustomSelect } from 'components/NewSelect';
import TextToolTip from 'components/TextToolTip';
import { useGetFieldKeys } from 'hooks/dynamicVariables/useGetFieldKeys';
import { useGetFieldValues } from 'hooks/dynamicVariables/useGetFieldValues';
import useDebounce from 'hooks/useDebounce';
import { isRetryableError } from 'utils/errorUtils';
import {
DYNAMIC_SIGNAL_LABEL,
DYNAMIC_SIGNALS,
type DynamicSignalOption,
signalForApi,
} from '../variableFormModel';
import { TELEMETRY_SIGNALS, type TelemetrySignal } from '../variableModel';
import styles from './VariableForm.module.scss';
interface DynamicVariableFieldsProps {
attribute: string;
signal: DynamicSignalOption;
signal: TelemetrySignal;
onChange: (patch: {
dynamicAttribute?: string;
dynamicSignal?: DynamicSignalOption;
dynamicSignal?: TelemetrySignal;
}) => void;
onPreview: (values: (string | number)[]) => void;
/** Inline error shown under the attribute field (e.g. duplicate attribute). */
attributeError?: string;
}
/** Dynamic-variable body: telemetry signal + field, whose live values preview. */
@@ -37,24 +27,18 @@ function DynamicVariableFields({
signal,
onChange,
onPreview,
attributeError,
}: DynamicVariableFieldsProps): JSX.Element {
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 300);
const apiSignal = signalForApi(signal);
const {
data: keyData,
isLoading,
error,
refetch,
} = useGetFieldKeys({
signal: apiSignal,
const { data: keyData, isLoading } = useGetFieldKeys({
signal,
name: debouncedSearch || undefined,
});
// `keys` is a Record keyed BY field name; the field names are the map keys.
// CustomSelect filters the supplied options locally as the user types.
// When the API reports the list is `complete`, search filters locally.
const isComplete = keyData?.data?.complete === true;
const options = useMemo(
() =>
Object.keys(keyData?.data?.keys ?? {}).map((name) => ({
@@ -65,7 +49,7 @@ function DynamicVariableFields({
);
const { data: valueData } = useGetFieldValues({
signal: apiSignal,
signal,
name: attribute,
enabled: !!attribute,
});
@@ -78,60 +62,40 @@ function DynamicVariableFields({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [valueData]);
const errorMessage = error ? (error as Error).message || null : null;
return (
<>
<div className={cx(styles.row, styles.sortSection)}>
<div className={cx(styles.labelContainer, styles.sourceLabel)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>Source</Typography.Text>
<TextToolTip
text="By default, this searches across logs, traces, and metrics, which can be slow. Selecting a single source improves performance. Many fields share the same values across different signals (for example, `k8s.pod.name` is identical in logs, traces and metrics) making one source enough. Only use `All telemetry` when you need fields that have different values in different signal types."
useFilledIcon={false}
outlinedIcon={<Info size={14} />}
/>
</div>
<Select
<SelectSimple
className={styles.sortSelect}
popupMatchSelectWidth={false}
value={signal}
options={DYNAMIC_SIGNALS.map((s) => ({
label: DYNAMIC_SIGNAL_LABEL[s],
value: s,
}))}
items={TELEMETRY_SIGNALS.map((s) => ({ label: s, value: s }))}
onChange={(value): void =>
onChange({ dynamicSignal: value as DynamicSignalOption })
onChange({ dynamicSignal: value as TelemetrySignal })
}
data-testid="variable-signal-select"
testId="variable-signal-select"
/>
</div>
<div className={cx(styles.row, styles.sortSection)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>Attribute</Typography.Text>
</div>
<CustomSelect
<Select
className={styles.searchSelect}
showSearch
value={attribute || undefined}
placeholder="Select a telemetry field"
loading={isLoading}
options={options}
filterOption={isComplete}
onSearch={setSearch}
onChange={(value): void => onChange({ dynamicAttribute: value as string })}
noDataMessage="No fields found"
errorMessage={errorMessage}
onRetry={(): void => {
void refetch();
}}
showRetryButton={error ? isRetryableError(error) : true}
options={options}
notFoundContent={isLoading ? 'Loading…' : 'No fields found'}
data-testid="variable-field-select"
/>
</div>
{attributeError ? (
<Typography.Text className={styles.errorText}>
{attributeError}
</Typography.Text>
) : null}
</>
);
}

View File

@@ -1,139 +0,0 @@
import { Badge } from '@signozhq/ui/badge';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
// eslint-disable-next-line signoz/no-antd-components -- fixed-option sort picker
import { Select } from 'antd';
import { CustomSelect } from 'components/NewSelect';
import {
VARIABLE_SORT_LABEL,
VARIABLE_SORTS,
type VariableFormModel,
type VariableSort,
} from '../variableFormModel';
import styles from './VariableForm.module.scss';
interface ListVariableFieldsProps {
model: VariableFormModel;
onChange: (patch: Partial<VariableFormModel>) => void;
previewValues: (string | number)[];
previewError: string | null;
defaultValue: string;
onDefaultValueChange: (value: string) => void;
/** Whether the "ALL values" toggle applies to this type (QUERY / CUSTOM). */
showAllOptionField: boolean;
}
/**
* Rows shared by the list-style variables (Query / Custom / Dynamic): the value
* preview, sort, multi-select / ALL toggles and the default-value picker.
*/
function ListVariableFields({
model,
onChange,
previewValues,
previewError,
defaultValue,
onDefaultValueChange,
showAllOptionField,
}: ListVariableFieldsProps): JSX.Element {
return (
<>
<div className={cx(styles.row, styles.previewSection)}>
<Typography.Text className={styles.previewLabel}>
Preview of Values
</Typography.Text>
<div className={styles.previewValues}>
{previewError ? (
<Typography.Text className={styles.previewError}>
{previewError}
</Typography.Text>
) : (
previewValues.map((value, idx) => (
<Badge
// eslint-disable-next-line react/no-array-index-key -- preview values are display-only and may contain duplicates
key={`${value}-${idx}`}
color="vanilla"
>
{value.toString()}
</Badge>
))
)}
</div>
</div>
<div className={cx(styles.row, styles.sortSection)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>Sort Values</Typography.Text>
</div>
<Select
className={styles.sortSelect}
popupMatchSelectWidth={false}
value={model.sort}
options={VARIABLE_SORTS.map((sort) => ({
label: VARIABLE_SORT_LABEL[sort],
value: sort,
}))}
onChange={(value): void => onChange({ sort: value as VariableSort })}
data-testid="variable-sort-select"
/>
</div>
<div className={cx(styles.row, styles.multiSection)}>
<Typography.Text className={styles.rowLabel}>
Enable multiple values to be checked
</Typography.Text>
<Switch
value={model.multiSelect}
onChange={(checked): void =>
onChange({
multiSelect: checked,
showAllOption: checked ? model.showAllOption : false,
})
}
testId="variable-multi-switch"
/>
</div>
{model.multiSelect && showAllOptionField ? (
<div className={cx(styles.row, styles.allOptionSection)}>
<Typography.Text className={styles.rowLabel}>
Include an option for ALL values
</Typography.Text>
<Switch
value={model.showAllOption}
onChange={(checked): void => onChange({ showAllOption: checked })}
testId="variable-all-switch"
/>
</div>
) : null}
<div className={cx(styles.row, styles.defaultValueSection)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>Default Value</Typography.Text>
<Typography.Text className={styles.defaultValueDesc}>
{model.type === 'QUERY'
? 'Click Test Run Query to see the values or add custom value'
: 'Select a value from the preview values or add custom value'}
</Typography.Text>
</div>
<CustomSelect
className={styles.searchSelect}
showSearch
allowClear
placeholder="Select a default value"
value={defaultValue || undefined}
onChange={(value): void => onDefaultValueChange((value as string) ?? '')}
options={previewValues.map((value) => ({
label: value.toString(),
value: value.toString(),
}))}
data-testid="variable-default-select"
/>
</div>
</>
);
}
export default ListVariableFields;

View File

@@ -3,14 +3,14 @@ import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import Editor from 'components/Editor';
import type { PayloadVariables } from 'types/api/dashboard/variables/query';
import sortValues from 'lib/dashboardVariables/sortVariableValues';
import type { VariableSort } from '../variableModel';
import styles from './VariableForm.module.scss';
interface QueryVariableFieldsProps {
queryValue: string;
/** Sibling variable selections, so dependent `$vars` in the query resolve. */
variables: PayloadVariables;
sort: VariableSort;
onChange: (queryValue: string) => void;
onPreview: (values: (string | number)[]) => void;
onError: (message: string | null) => void;
@@ -19,7 +19,7 @@ interface QueryVariableFieldsProps {
/** Query-variable body: SQL editor + "Test Run Query" that previews the values. */
function QueryVariableFields({
queryValue,
variables,
sort,
onChange,
onPreview,
onError,
@@ -30,21 +30,20 @@ function QueryVariableFields({
setIsRunning(true);
onError(null);
try {
const res = await dashboardVariablesQuery({ query: queryValue, variables });
const res = await dashboardVariablesQuery({
query: queryValue,
variables: {},
});
if (res.statusCode === 200 && res.payload) {
onPreview(res.payload.variableValues ?? []);
onPreview(
sortValues(res.payload.variableValues ?? [], sort) as (string | number)[],
);
} else {
onError(res.error || 'Failed to run query');
onPreview([]);
}
} catch (err) {
// `dashboardVariablesQuery` throws `{ message, details: { error } }`.
const detail = (err as { details?: { error?: string } }).details?.error;
const message =
detail && detail.includes('Syntax error:')
? 'Please make sure query is valid and dependent variables are selected'
: detail || (err as Error).message || 'Failed to run query';
onError(message);
onError((err as Error).message || 'Failed to run query');
onPreview([]);
} finally {
setIsRunning(false);

View File

@@ -5,8 +5,22 @@
.container {
display: flex;
flex-direction: column;
border: 1px solid var(--l1-border);
border-radius: 3px;
border: 1px solid var(--l2-border);
}
.allVariables {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-bottom: 1px solid var(--l1-border);
}
.allVariablesBtn {
--button-height: 24px;
--button-padding: 0;
color: var(--muted-foreground);
}
.content {
@@ -28,12 +42,6 @@
width: 200px;
}
.sourceLabel {
display: flex;
align-items: center;
gap: 6px;
}
.label {
color: var(--l2-foreground);
font-family: Inter;
@@ -51,7 +59,7 @@
.textarea,
.defaultInput {
padding: 6px 6px 6px 8px;
border: 1px solid var(--l2-border);
border: 1px solid var(--l1-border);
border-radius: 2px;
background: var(--l3-background);
}
@@ -70,89 +78,48 @@
color: var(--bg-amber-500);
}
/* Variable type — Tabs root composing the picker row + per-type body panels. */
/* Variable type segmented group */
.typeSection {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 40px;
margin-bottom: 0;
}
/* Picker row (label left, tabs right); the bottom divider separates type from
config. Single line — the tab row scrolls (never wraps) when narrow. */
.typePicker {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--l2-border);
@media (max-width: 1440px) {
flex-wrap: wrap;
}
}
/* Active tab panel — reset the Tabs default padding; body rows handle spacing. */
.typePanel {
padding: 0 !important;
}
.typeContent {
display: flex;
flex-direction: column;
gap: 20px;
}
.typeLabelContainer {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
width: auto;
}
/* Horizontal scroll so the tab row never wraps to a second line. The scrollbar
is hidden — the row stays a single crisp line and scrolls only when narrow. */
.typeTabsScroll {
justify-self: flex-end;
--tab-list-wrapper-secondary-padding-left: 0;
}
/* Connected segmented control, mirroring Overview's SegmentedControl: no outer
padding, segments divided by 1px borders, active segment filled + bold. */
.typeTabs {
display: inline-flex;
flex-wrap: nowrap;
width: max-content;
gap: 0;
padding: 0;
border: 1px solid var(--l2-border);
border-radius: 2px;
background: transparent;
}
.typeTab {
display: inline-flex;
.typeBtnGroup {
display: grid;
grid-template-columns: repeat(4, max-content);
height: 32px;
flex-shrink: 0;
border: 1px solid var(--l1-border);
border-radius: 2px;
background: var(--l2-background);
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1);
}
.typeBtn {
--button-height: 32px;
display: flex;
align-items: center;
gap: 6px;
min-height: 24px;
padding: 6px 14px;
white-space: nowrap;
justify-content: center;
gap: 4px;
min-width: 114px;
border-radius: 0;
color: var(--l2-foreground);
&:not(:last-child) {
border-right: 1px solid var(--l2-border);
& + & {
border-left: 1px solid var(--l1-border);
}
}
&[data-state='active'] {
color: var(--l1-foreground);
font-weight: 500;
// override the Tabs component's default (transparent) active background.
background: var(--l3-background) !important;
}
.typeBtnSelected {
background: var(--l1-border);
color: var(--l1-foreground);
}
.betaTag {
@@ -171,7 +138,7 @@
.editorWrap {
height: 240px;
overflow: hidden;
border: 1px solid var(--l2-border);
border: 1px solid var(--l1-border);
border-radius: 2px;
}
@@ -187,7 +154,7 @@
.customSection :global(.custom-collapse) {
width: 100%;
border: 1px solid var(--l2-border);
border: 1px solid var(--l1-border);
border-radius: 3px 3px 0 0;
:global(.ant-collapse-item) {
@@ -241,7 +208,7 @@
min-height: 88px;
margin-bottom: 0;
padding-bottom: 8px;
border: 1px solid var(--l2-border);
border: 1px solid var(--l1-border);
border-radius: 3px;
}
@@ -304,9 +271,13 @@
letter-spacing: -0.07px;
}
.sortSelect {
width: 192px;
}
.defaultValueSection {
display: flex;
justify-content: space-between;
display: grid;
grid-template-columns: max-content 1fr;
gap: 1rem;
align-items: center;
margin-bottom: 0;
@@ -326,21 +297,14 @@
letter-spacing: -0.06px;
}
/* All variable selects (Source / Attribute / Sort / Default Value) share width
and a consistent --l2-border outline. */
.sortSelect,
.searchSelect {
width: 240px;
flex-shrink: 0;
:global(.ant-select-selector) {
border-color: var(--l2-border) !important;
}
width: 100%;
}
.actionButtons {
width: 100%;
/* Footer */
.footer {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 12px;
}

View File

@@ -1,199 +1,350 @@
import { Check, X } from '@signozhq/icons';
import { useEffect, useState } from 'react';
import { ArrowLeft, Check, X } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { TabsContent, TabsRoot } from '@signozhq/ui/tabs';
import { SelectSimple } from '@signozhq/ui/select';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
// eslint-disable-next-line signoz/no-antd-components -- TextArea/Collapse: no @signozhq/ui equivalent
import { Collapse, Input as AntdInput } from 'antd';
// eslint-disable-next-line signoz/no-antd-components -- TextArea/Collapse/searchable Select: no @signozhq/ui equivalent
import { Collapse, Input as AntdInput, Select } from 'antd';
import { commaValuesParser } from 'lib/dashboardVariables/customCommaValuesParser';
import sortValues from 'lib/dashboardVariables/sortVariableValues';
import type { VariableType } from '../variableFormModel';
import {
VARIABLE_SORTS,
type VariableFormModel,
type VariableSort,
type VariableType,
} from '../variableModel';
import DynamicVariableFields from './DynamicVariableFields';
import ListVariableFields from './ListVariableFields';
import QueryVariableFields from './QueryVariableFields';
import { useVariableForm } from './useVariableForm';
import VariableTypeTabs from './VariableTypeTabs';
import VariableTypeSelector from './VariableTypeSelector';
import styles from './VariableForm.module.scss';
import BackToAllVariables from '../components/BackToAllVariables/BackToAllVariables';
import { VariableFormProps } from '../types';
import VariableInfoForm from '../components/VariableInfoForm/VariableInfoForm';
const SORT_LABEL: Record<VariableSort, string> = {
DISABLED: 'Disabled',
ASC: 'Ascending',
DESC: 'Descending',
};
function getNameError(name: string, existingNames: string[]): string | null {
if (name === '') {
return 'Variable name is required';
}
if (/\s/.test(name)) {
return 'Variable name cannot contain whitespaces';
}
if (existingNames.includes(name)) {
return 'Variable name already exists';
}
return null;
}
interface VariableFormProps {
initial: VariableFormModel;
/** Names of the other variables, for uniqueness validation. */
existingNames: string[];
isSaving: boolean;
onClose: () => void;
onSave: (model: VariableFormModel) => void;
}
/**
* In-drawer variable editor reproducing the V1 VariableItem layout, built on
* @signozhq components (antd kept only for the monaco editor, TextArea, Collapse
* and searchable selects). Master→detail: renders in place of the list. Form
* state/handlers live in {@link useVariableForm}; the shared list-type rows in
* {@link ListVariableFields}.
* and searchable selects). Master→detail: renders in place of the list.
*/
function VariableForm({
initial,
siblings,
isNew,
existingNames,
isSaving,
onClose,
onSave,
}: VariableFormProps): JSX.Element {
const {
model,
set,
onNameChange,
selectType,
onCustomChange,
onDynamicChange,
setRawPreview,
previewValues,
previewError,
setPreviewError,
defaultValue,
setDefaultValue,
visibleNameError,
nameError,
attributeError,
cycleError,
isListType,
showAllOptionField,
payloadVariables,
handleSave,
} = useVariableForm({ initial, siblings, isNew, onSave });
const [model, setModel] = useState<VariableFormModel>(initial);
const [previewValues, setPreviewValues] = useState<(string | number)[]>([]);
const [previewError, setPreviewError] = useState<string | null>(null);
const [defaultValue, setDefaultValue] = useState<string>(
((initial.defaultValue as { value?: string })?.value ?? '') as string,
);
// Shared list rows (preview/sort/multi/default) for the list-type variables;
// rendered as a sibling inside each list-type panel. Only the active panel
// mounts (Tabs unmounts the rest), so reusing one element is safe.
const listFields = isListType ? (
<ListVariableFields
model={model}
onChange={set}
previewValues={previewValues}
previewError={previewError}
defaultValue={defaultValue}
onDefaultValueChange={setDefaultValue}
showAllOptionField={showAllOptionField}
/>
) : null;
useEffect(() => {
setModel(initial);
setPreviewValues([]);
setPreviewError(null);
setDefaultValue(
((initial.defaultValue as { value?: string })?.value ?? '') as string,
);
}, [initial]);
const set = (patch: Partial<VariableFormModel>): void =>
setModel((prev) => ({ ...prev, ...patch }));
const selectType = (type: VariableType): void => {
set({ type });
setPreviewValues([]);
setPreviewError(null);
};
const onCustomChange = (value: string): void => {
set({ customValue: value });
setPreviewValues(
sortValues(commaValuesParser(value), model.sort) as (string | number)[],
);
};
const trimmedName = model.name.trim();
const nameError = getNameError(trimmedName, existingNames);
const isListType =
model.type === 'QUERY' || model.type === 'CUSTOM' || model.type === 'DYNAMIC';
const showAllOptionField = model.type === 'QUERY' || model.type === 'CUSTOM';
const handleSave = (): void => {
onSave({
...model,
name: trimmedName,
defaultValue: defaultValue ? { value: defaultValue } : undefined,
});
};
return (
<div className={styles.container}>
<BackToAllVariables onClose={onClose} />
<>
<div className={styles.container}>
<div className={styles.allVariables}>
<Button
variant="ghost"
color="secondary"
className={styles.allVariablesBtn}
prefix={<ArrowLeft size={14} />}
onClick={onClose}
testId="variable-form-back"
>
All variables
</Button>
</div>
<div className={styles.content}>
<VariableInfoForm
title={model.name}
description={model.description}
onTitleChange={onNameChange}
onDescriptionChange={(value): void => set({ description: value })}
visibleNameError={visibleNameError}
/>
<div className={styles.content}>
{/* Name */}
<div className={cx(styles.row, styles.column)}>
<Typography.Text className={styles.label}>Name</Typography.Text>
<Input
className={styles.input}
value={model.name}
placeholder="Unique name of the variable"
onChange={(e): void => set({ name: e.target.value })}
testId="variable-name-input"
/>
{nameError ? (
<Typography.Text className={styles.errorText}>
{nameError}
</Typography.Text>
) : null}
</div>
<TabsRoot
className={styles.typeSection}
value={model.type}
onValueChange={(next): void => selectType(next as VariableType)}
>
<VariableTypeTabs />
{/* Description */}
<div className={cx(styles.row, styles.column)}>
<Typography.Text className={styles.label}>Description</Typography.Text>
<AntdInput.TextArea
className={styles.textarea}
value={model.description}
placeholder="Enter a description for the variable"
rows={3}
onChange={(e): void => set({ description: e.target.value })}
data-testid="variable-description-input"
/>
</div>
<TabsContent value="DYNAMIC" className={styles.typePanel}>
<div className={styles.typeContent}>
<DynamicVariableFields
attribute={model.dynamicAttribute}
signal={model.dynamicSignal}
onChange={onDynamicChange}
onPreview={setRawPreview}
attributeError={attributeError}
{/* Variable Type */}
<VariableTypeSelector value={model.type} onChange={selectType} />
{/* Type-specific body */}
{model.type === 'DYNAMIC' ? (
<DynamicVariableFields
attribute={model.dynamicAttribute}
signal={model.dynamicSignal}
onChange={(patch): void => set(patch)}
onPreview={setPreviewValues}
/>
) : null}
{model.type === 'QUERY' ? (
<QueryVariableFields
queryValue={model.queryValue}
sort={model.sort}
onChange={(queryValue): void => set({ queryValue })}
onPreview={setPreviewValues}
onError={setPreviewError}
/>
) : null}
{model.type === 'CUSTOM' ? (
<div className={cx(styles.row, styles.customSection)}>
<Collapse
collapsible="header"
rootClassName="custom-collapse"
defaultActiveKey={['1']}
items={[
{
key: '1',
label: 'Options',
children: (
<AntdInput.TextArea
value={model.customValue}
placeholder="Enter options separated by commas."
rootClassName="comma-input"
onChange={(e): void => onCustomChange(e.target.value)}
data-testid="variable-custom-input"
/>
),
},
]}
/>
{listFields}
</div>
</TabsContent>
) : null}
<TabsContent value="QUERY" className={styles.typePanel}>
<div className={styles.typeContent}>
<QueryVariableFields
queryValue={model.queryValue}
variables={payloadVariables}
onChange={(queryValue): void => set({ queryValue })}
onPreview={setRawPreview}
onError={setPreviewError}
{model.type === 'TEXT' ? (
<div className={cx(styles.row, styles.textboxSection)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>
Default Value
</Typography.Text>
</div>
<Input
className={styles.defaultInput}
value={model.textValue}
placeholder="Enter a default value (if any)..."
onChange={(e): void => set({ textValue: e.target.value })}
testId="variable-text-input"
/>
{listFields}
</div>
</TabsContent>
) : null}
<TabsContent value="CUSTOM" className={styles.typePanel}>
<div className={styles.typeContent}>
<div className={cx(styles.row, styles.customSection)}>
<Collapse
collapsible="header"
rootClassName="custom-collapse"
defaultActiveKey={['1']}
items={[
{
key: '1',
label: 'Options',
children: (
<AntdInput.TextArea
value={model.customValue}
placeholder="Enter options separated by commas."
rootClassName="comma-input"
onChange={(e): void => onCustomChange(e.target.value)}
data-testid="variable-custom-input"
/>
),
},
]}
{/* Shared rows for list-type variables */}
{isListType ? (
<>
<div className={cx(styles.row, styles.previewSection)}>
<Typography.Text className={styles.previewLabel}>
Preview of Values
</Typography.Text>
<div className={styles.previewValues}>
{previewError ? (
<Typography.Text className={styles.previewError}>
{previewError}
</Typography.Text>
) : (
previewValues.map((value, idx) => (
<Badge
// eslint-disable-next-line react/no-array-index-key -- preview values are display-only and may contain duplicates
key={`${value}-${idx}`}
color="vanilla"
>
{value.toString()}
</Badge>
))
)}
</div>
</div>
<div className={cx(styles.row, styles.sortSection)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>Sort Values</Typography.Text>
</div>
<SelectSimple
className={styles.sortSelect}
value={model.sort}
items={VARIABLE_SORTS.map((sort) => ({
label: SORT_LABEL[sort],
value: sort,
}))}
onChange={(value): void => set({ sort: value as VariableSort })}
testId="variable-sort-select"
/>
</div>
{listFields}
</div>
</TabsContent>
<TabsContent value="TEXT" className={styles.typePanel}>
<div className={styles.typeContent}>
<div className={cx(styles.row, styles.textboxSection)}>
<div className={cx(styles.row, styles.multiSection)}>
<Typography.Text className={styles.rowLabel}>
Enable multiple values to be checked
</Typography.Text>
<Switch
value={model.multiSelect}
onChange={(checked): void => {
set({
multiSelect: checked,
showAllOption: checked ? model.showAllOption : false,
});
}}
testId="variable-multi-switch"
/>
</div>
{model.multiSelect && showAllOptionField ? (
<div className={cx(styles.row, styles.allOptionSection)}>
<Typography.Text className={styles.rowLabel}>
Include an option for ALL values
</Typography.Text>
<Switch
value={model.showAllOption}
onChange={(checked): void => set({ showAllOption: checked })}
testId="variable-all-switch"
/>
</div>
) : null}
<div className={cx(styles.row, styles.defaultValueSection)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>
Default Value
</Typography.Text>
<Typography.Text className={styles.defaultValueDesc}>
{model.type === 'QUERY'
? 'Click Test Run Query to see the values or add custom value'
: 'Select a value from the preview values or add custom value'}
</Typography.Text>
</div>
<Input
className={styles.defaultInput}
value={model.textValue}
placeholder="Enter a default value (if any)..."
onChange={(e): void => set({ textValue: e.target.value })}
testId="variable-text-input"
<Select
className={styles.searchSelect}
showSearch
allowClear
placeholder="Select a default value"
value={defaultValue || undefined}
onChange={(value): void => setDefaultValue(value ?? '')}
options={previewValues.map((value) => ({
label: value.toString(),
value: value.toString(),
}))}
data-testid="variable-default-select"
/>
</div>
</div>
</TabsContent>
</TabsRoot>
{cycleError ? (
<Typography.Text className={styles.errorText}>
{cycleError}
</Typography.Text>
) : null}
<div className={styles.actionButtons}>
<Button
variant="outlined"
color="secondary"
prefix={<X size={14} />}
onClick={onClose}
>
Discard
</Button>
<Button
variant="solid"
color="primary"
prefix={<Check size={14} />}
disabled={!!nameError || !!attributeError}
loading={isSaving}
onClick={handleSave}
testId="variable-save"
>
Save Variable
</Button>
</>
) : null}
</div>
</div>
</div>
<div className={styles.footer}>
<Button
variant="solid"
color="secondary"
prefix={<X size={14} />}
onClick={onClose}
>
Discard
</Button>
<Button
variant="solid"
color="primary"
prefix={<Check size={14} />}
disabled={!!nameError}
loading={isSaving}
onClick={handleSave}
testId="variable-save"
>
Save Variable
</Button>
</div>
</>
);
}

View File

@@ -0,0 +1,99 @@
import {
ClipboardType,
DatabaseZap,
Info,
LayoutList,
Pyramid,
} from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip';
import type { VariableType } from '../variableModel';
import styles from './VariableForm.module.scss';
interface VariableTypeSelectorProps {
value: VariableType;
onChange: (type: VariableType) => void;
}
/** The segmented Dynamic / Textbox / Custom / Query type picker. */
function VariableTypeSelector({
value,
onChange,
}: VariableTypeSelectorProps): JSX.Element {
return (
<div className={cx(styles.row, styles.typeSection)}>
<div className={styles.typeLabelContainer}>
<Typography.Text className={styles.label}>Variable Type</Typography.Text>
<TextToolTip
text="Learn more about supported variable types"
url="https://signoz.io/docs/userguide/manage-variables/#supported-variable-types"
urlText="here"
useFilledIcon={false}
outlinedIcon={<Info size={14} />}
/>
</div>
<div className={styles.typeBtnGroup}>
<Button
variant="ghost"
color="secondary"
prefix={<Pyramid size={14} />}
className={cx(styles.typeBtn, {
[styles.typeBtnSelected]: value === 'DYNAMIC',
})}
onClick={(): void => onChange('DYNAMIC')}
testId="variable-type-dynamic"
>
Dynamic
<Badge color="robin" className={styles.betaTag}>
Beta
</Badge>
</Button>
<Button
variant="ghost"
color="secondary"
prefix={<ClipboardType size={14} />}
className={cx(styles.typeBtn, {
[styles.typeBtnSelected]: value === 'TEXT',
})}
onClick={(): void => onChange('TEXT')}
testId="variable-type-textbox"
>
Textbox
</Button>
<Button
variant="ghost"
color="secondary"
prefix={<LayoutList size={14} />}
className={cx(styles.typeBtn, {
[styles.typeBtnSelected]: value === 'CUSTOM',
})}
onClick={(): void => onChange('CUSTOM')}
testId="variable-type-custom"
>
Custom
</Button>
<Button
variant="ghost"
color="secondary"
prefix={<DatabaseZap size={14} />}
className={cx(styles.typeBtn, {
[styles.typeBtnSelected]: value === 'QUERY',
})}
onClick={(): void => onChange('QUERY')}
testId="variable-type-query"
>
Query
<Badge color="amber" className={styles.betaTag}>
Not Recommended
</Badge>
</Button>
</div>
</div>
);
}
export default VariableTypeSelector;

View File

@@ -1,93 +0,0 @@
import {
ClipboardType,
DatabaseZap,
Info,
LayoutList,
Pyramid,
} from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { TabsList, TabsTrigger } from '@signozhq/ui/tabs';
import { Typography } from '@signozhq/ui/typography';
import TextToolTip from 'components/TextToolTip';
import styles from './VariableForm.module.scss';
/**
* Presentational trigger row for the variable-type tabs (label + segmented
* triggers). Must render inside a `TabsRoot`, which owns the active state and
* change handling; the matching `TabsContent` panels are siblings in the root.
*/
function VariableTypeTabs(): JSX.Element {
return (
<div className={styles.typePicker}>
<div className={styles.typeLabelContainer}>
<Typography.Text className={styles.label}>Variable Type</Typography.Text>
<TextToolTip
text="Learn more about supported variable types"
url="https://signoz.io/docs/userguide/manage-variables/#supported-variable-types"
urlText="here"
useFilledIcon={false}
outlinedIcon={<Info size={14} />}
/>
</div>
<div className={styles.typeTabsScroll}>
<TabsList variant="secondary" className={styles.typeTabs}>
<TabsTrigger
value="DYNAMIC"
className={styles.typeTab}
testId="variable-type-dynamic"
>
<Pyramid size={14} />
Dynamic
<Badge color="robin" className={styles.betaTag}>
Beta
</Badge>
</TabsTrigger>
<TabsTrigger
value="TEXT"
className={styles.typeTab}
testId="variable-type-textbox"
>
<ClipboardType size={14} />
Textbox
</TabsTrigger>
<TabsTrigger
value="CUSTOM"
className={styles.typeTab}
testId="variable-type-custom"
>
<LayoutList size={14} />
Custom
</TabsTrigger>
<TabsTrigger
value="QUERY"
className={styles.typeTab}
testId="variable-type-query"
>
<DatabaseZap size={14} />
Query
<Badge color="amber" className={styles.betaTag}>
Not Recommended
</Badge>
<span
className={styles.betaTag}
onClick={(e): void => e.stopPropagation()}
role="presentation"
>
<TextToolTip
text="Learn why we don't recommend"
url="https://signoz.io/docs/userguide/manage-variables/#why-avoid-clickhouse-query-variables"
urlText="here"
useFilledIcon={false}
outlinedIcon={<Info size={14} />}
/>
</span>
</TabsTrigger>
</TabsList>
</div>
</div>
);
}
export default VariableTypeTabs;

View File

@@ -1,191 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import { commaValuesParser } from 'lib/dashboardVariables/customCommaValuesParser';
import type { PayloadVariables } from 'types/api/dashboard/variables/query';
import type { VariableSelectionMap } from '../../../VariablesBar/selectionTypes';
import { useDashboardStore } from '../../../store/useDashboardStore';
import { detectVariableCycle } from '../variableDependencies';
import {
sortValuesByOrder,
type VariableFormModel,
type VariableType,
} from '../variableFormModel';
import { getAttributeError, getNameError } from './variableValidation';
// Stable reference so the zustand selector never returns a fresh object (which
// would make useSyncExternalStore loop) when this dashboard has no selections.
const EMPTY_SELECTIONS: VariableSelectionMap = {};
interface UseVariableFormArgs {
initial: VariableFormModel;
siblings: VariableFormModel[];
isNew: boolean;
onSave: (model: VariableFormModel) => void;
}
export interface UseVariableForm {
model: VariableFormModel;
set: (patch: Partial<VariableFormModel>) => void;
onNameChange: (value: string) => void;
selectType: (type: VariableType) => void;
onCustomChange: (value: string) => void;
onDynamicChange: (patch: Partial<VariableFormModel>) => void;
setRawPreview: (values: (string | number)[]) => void;
previewValues: (string | number)[];
previewError: string | null;
setPreviewError: (message: string | null) => void;
defaultValue: string;
setDefaultValue: (value: string) => void;
visibleNameError: string | null;
nameError: string | null;
attributeError: string | undefined;
cycleError: string | null;
isListType: boolean;
showAllOptionField: boolean;
payloadVariables: PayloadVariables;
handleSave: () => void;
}
const readDefaultValue = (model: VariableFormModel): string =>
((model.defaultValue as { value?: string })?.value ?? '') as string;
/** Form state, derivations and handlers for the variable editor. */
export function useVariableForm({
initial,
siblings,
isNew,
onSave,
}: UseVariableFormArgs): UseVariableForm {
const [model, setModel] = useState<VariableFormModel>(initial);
// Raw, unsorted preview; `previewValues` applies the chosen sort so a shown
// preview re-sorts when Sort changes.
const [rawPreview, setRawPreview] = useState<(string | number)[]>([]);
const [previewError, setPreviewError] = useState<string | null>(null);
const [cycleError, setCycleError] = useState<string | null>(null);
// In add mode, mirror the chosen attribute into the name until the user types.
const [nameTouched, setNameTouched] = useState(false);
const [defaultValue, setDefaultValue] = useState<string>(
readDefaultValue(initial),
);
useEffect(() => {
setModel(initial);
setRawPreview([]);
setPreviewError(null);
setCycleError(null);
setNameTouched(false);
setDefaultValue(readDefaultValue(initial));
}, [initial]);
const set = (patch: Partial<VariableFormModel>): void =>
setModel((prev) => ({ ...prev, ...patch }));
const previewValues = useMemo(
() => sortValuesByOrder(rawPreview, model.sort) as (string | number)[],
[rawPreview, model.sort],
);
const existingNames = useMemo(() => siblings.map((v) => v.name), [siblings]);
const existingDynamicAttributes = useMemo(
() =>
siblings
.filter((v) => v.type === 'DYNAMIC' && v.dynamicAttribute)
.map((v) => v.dynamicAttribute),
[siblings],
);
// Sibling selections feed the Query "Test Run" so dependent `$vars` resolve.
const dashboardId = useDashboardStore((s) => s.dashboardId);
const selections = useDashboardStore(
(s) => s.variableValues[dashboardId ?? ''] ?? EMPTY_SELECTIONS,
);
const payloadVariables = useMemo<PayloadVariables>(() => {
const out: PayloadVariables = {};
siblings.forEach((v) => {
if (v.name) {
out[v.name] = selections[v.name]?.value ?? null;
}
});
return out;
}, [siblings, selections]);
const trimmedName = model.name.trim();
const nameError = getNameError(trimmedName, existingNames, initial.name);
// Surface the message only once the field is dirty; Save stays disabled regardless.
const visibleNameError = nameTouched ? nameError : null;
const attributeError = getAttributeError(model, existingDynamicAttributes);
const isListType =
model.type === 'QUERY' || model.type === 'CUSTOM' || model.type === 'DYNAMIC';
const showAllOptionField = model.type === 'QUERY' || model.type === 'CUSTOM';
const onNameChange = (value: string): void => {
setNameTouched(true);
set({ name: value });
};
const selectType = (type: VariableType): void => {
set({ type });
setRawPreview([]);
setPreviewError(null);
};
const onCustomChange = (value: string): void => {
set({ customValue: value });
setRawPreview(commaValuesParser(value));
};
// In add mode, mirror the selected attribute into the name until the user
// edits the name themselves (matches the V1 dynamic-variable behaviour).
const onDynamicChange = (patch: Partial<VariableFormModel>): void => {
if (isNew && !nameTouched && patch.dynamicAttribute) {
set({ ...patch, name: patch.dynamicAttribute });
} else {
set(patch);
}
};
const handleSave = (): void => {
const next: VariableFormModel = {
...model,
name: trimmedName,
defaultValue: defaultValue ? { value: defaultValue } : undefined,
};
const cycle = detectVariableCycle([...siblings, next]);
if (cycle) {
setCycleError(
`Cannot save: circular dependency detected between variables: ${cycle.join(
' → ',
)}`,
);
return;
}
setCycleError(null);
onSave(next);
};
return {
model,
set,
onNameChange,
selectType,
onCustomChange,
onDynamicChange,
setRawPreview,
previewValues,
previewError,
setPreviewError,
defaultValue,
setDefaultValue,
visibleNameError,
nameError,
attributeError,
cycleError,
isListType,
showAllOptionField,
payloadVariables,
handleSave,
};
}

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