Compare commits

..

10 Commits

Author SHA1 Message Date
srikanthccv
8179d1a027 fix(alertmanager): resolve email SMTP settings from env 2026-07-26 18:00:15 +05:30
Naman Verma
e728942fec fix: handle malformed reduceTo + pick correct columns for traces in list panel (#12275)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
2026-07-25 14:57:29 +00:00
Naman Verma
5bf95534ad fix: reencode svg images to base64 when migrating dashboards (#12274)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-07-25 13:01:34 +00:00
Naman Verma
51819d6337 chore: add migration script from current to perses dashboard (#11712)
* chore: add migration script from current to perses dashboard

* fix: move WrapInV5Envelope to types package

* fix: generate internal name from title

* fix: match with lower case signal for variables

* fix: sanitize tags instead of throwing error

* fix: extract row and widget positions to build expanded sections

* chore: add a catch all panic check to log migration error

* chore: catch typecast errors

* fix: remove nil nil return

* test: fix ut to check for v2 internal name

* chore: remove spec md file

* chore: separate error type for migration

* feat: note down all errors in migration

* fix: allow zero value for threshold values

* fix: ignore react placeholders in layout

* fix: note error in query parsing

* fix: read threshold value properly during migration

* fix: adjust to schema changes in variables

* test: fix unit tests

* fix: catch error from decodeTelemetryFields

* fix: make threshold value non-nullable

* fix: handle list of widget IDs in panel maps

* fix: add better error note down for failed conversions

* fix: normalize having array from v4 schema

* fix: remove metric aggregation from non metric queries (malformed json)

* fix: normalize telemetry field keys in query

* chore: add temporary doc for other malformation handlers needed

* fix: add more malformed query handling

* fix: add more malformation handling

* chore: reuse transition package (adds dependency, need to see what to do)

* fix: fix go lint issue

* revert: remove md file

* fix: make none the default fill mode during migration

* fix: add nil check on limit value when normalizing page size

* fix: validate dashboard spec after migration

* fix: remove unused method

* fix: match shape safe version to existing version

* chore: improve variable names

* chore: better comments and names in layout migrator

* chore: add explanation comment for continuing on empty id widget

* fix: ditch dynamic vars with no attr, rearrange overlapping layouts

* fix: add default aggregation count() if none is present in logs/traces

* fix: add empty list tolerance to v1 variables

* fix: add fixes based on backup migration testing

* chore: skip over unreferenced widgets

* chore: remove widgets that have a single query that has no metric name

* chore: assign missing formula names like ui does

* chore: assign query names to queries if missing

* chore: drop legacy filter fields that ui also ignores

* chore: make migration drop widgets that have no aggregations for metrics

* chore: normalize group by

* chore: remove expressions from metric aggregations

* chore: add more error notes

* chore: replace em dash with hyphen in widget id

* chore: add more panel id sanitization

* chore: replace faulty order by keys with aggregation expression

* chore: fill valid span gaps

* chore: handle misformed threshold operators

* chore: remove repeat occurences of widgets in layouts

* chore: remove limits that cross max limit

* chore: restore function args malformed by v4->v5 migration

* chore: remove invalid functions

* chore: skip variables in wrong type

* chore: add wdiget id type check in layout migration

* chore: add a.count to malformed order by keys list

* chore: remove bad aggregations that accompany valid ones

* chore: drop trace operators with no expression

* chore: drop invalid formulas

* chore: skip unknown panel kind

* chore: drop widgets with >1 and all aggregations as malformed

* chore: skip empty type variables

* chore: pass threshold format raw without typecasting

* chore: add handling for trace operators around wrapinv5env

* chore: handle no string value for variable sort

* test: fix span gaps test

* chore: undo schema change in threshold value

* chore: final set of error fixes/hacks

* fix: fix build errors post merge conflicts

* test: remove test for floating pt value

* test: use appendf instead of sprintf

* feat: add scaffolding to migrate dashboards

* fix: fill in ref path when building grid layout

* feat: handle v1 payloads in v2 create api

* fix: return empty objects instead of null if no variables/panels/layouts are there

* test: fix unit test for empty layouts

* fix: add empty links array in migration

* test: add unit test to show v5  takes precedence over v4

* fix: change EXISTS to exists so that migrateCommon handles it properly

* test: add unit test for count with attributes

* fix: run order by handling after default aggregation handling

* fix: restore formula fields that wrapinv5envelope removes

* fix: keep thresholds that do not have an optional label

* fix: force allowAll to be false if allowMultiple is false

* fix: migrate name column even if data column migration fails

* fix: use slog for logging

* refactor(dashboard): drop ConvertAllV1ToV2 module scaffolding from base

Remove the temporary v1→v2 bulk-conversion module method (ConvertAllV1ToV2 +
migrateOneV1ToV2), its result types (V1ToV2MigrationResult / V1ToV2MigrationItem),
the Module interface entry, and the EE delegation. This scaffolding lives on the
migration feature branches, not the base branch that merges to main.

V1Name and store.UpdateName are kept (small helpers; UpdateName is on the shared
Store interface).

* fix: add image validation and base64 to asset conversion

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-07-25 08:41:07 +00:00
Nikhil Mantri
d22ee8f7aa fix(telemetrymetadata): update fallback to false for positive operators in related values queries (#12250)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
* chore: update fallback to false for positive operators

* chore: changed to distributed table

* chore: flip AND to or in outside condition

* chore: added data_source condition

* chore: added integration tests

* chore: simplified comment

* chore: more simplified comment

* chore: test case both columns contain host.name -> resources take precedence
2026-07-25 04:08:25 +00:00
Ashwin Bhatkal
5b004d1efc refactor(dashboard): store system icons as SVG assets with a safe image resolver (#12259)
* feat(dashboard-v2): add file-based dashboard icons and logos with a safe image resolver

* refactor(dashboard-v2): render dashboard icons via the safe resolver

* fix(dashboard-v2): clamp dashboard icon size to 16px

* fix(dashboard-v2): reject invalid dashboard image before saving

* fix(dashboard-v2): validate dashboard image server-side

* test(dashboard-v2): stub the Vite-only icon glob for jest and align image fixtures

* test(dashboard-v2): update patch test for image validation

* refactor(dashboard-v2): hoist dashboard image consts/regex and tidy validation tests

* refactor(dashboard-v2): drop the validateImage doc comment
2026-07-25 03:19:19 +00:00
Naman Verma
ecb77af4cb chore: fix schema and validations based on migration errors (#12263)
Some checks failed
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / staging (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
* chore: fix schema based on migration errors

* test: add rejection based integration tests for new validations

* fix: remove datasource field from schema
2026-07-24 15:37:04 +00:00
Tushar Vats
973429a22f fix(logs): stop hasToken and hasAny/hasAll 500s on separator and big-int needles (#12261)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
hasToken(body, <needle>) returned a 500 whenever the needle contained a
token separator or whitespace (ClickHouse code 36). Validate the needle
up front in conditionForHasToken (the single choke point for both legacy
and JSON modes) and return a clean 400 pointing at CONTAINS instead.

hasAny/hasAll on a legacy body array returned a 500 for a quoted integer
needle >= 2^32: clickhouse-go binds it as a concrete Array(UInt64), which
has no supertype with the Array(Nullable(Int64)) extraction that the
function must unify (code 386). CAST the needle array to Array(Int64) to
match the extraction. Scalar has() and the scalar OR-fallback are
value-level and unaffected.
2026-07-24 12:56:56 +00:00
Swapnil Nakade
9f5ad1ba26 fix: list gcp accounts was missing config (#12175)
* fix: list gcp accounts was missing config

* refactor: updating cloudintegration integration tests

* refactor: moving variables to test file

* chore: removing verbose comments
2026-07-24 10:56:33 +00:00
Naman Verma
b2574e7797 fix: remove requirement for links to be always present (#12260)
* fix: remove requirement for links to be always present

* fix: make links use defined type

* test: add integration test for link omission roundtrip

* test: change not in to None for links in panel

---------

Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-07-24 10:54:47 +00:00
158 changed files with 10109 additions and 2550 deletions

View File

@@ -39,6 +39,8 @@ jobs:
matrix:
suite:
- alerts
- alertmanager
- alertmanagerrotation
- basepath
- callbackauthn
- cloudintegrations

View File

@@ -1592,13 +1592,6 @@ components:
required:
- config
type: object
CommonDisplay:
properties:
description:
type: string
name:
type: string
type: object
CommonJSONRef:
properties:
$ref:
@@ -2732,11 +2725,6 @@ components:
type: object
DashboardtypesDashboardSpec:
properties:
datasources:
additionalProperties:
$ref: '#/components/schemas/DashboardtypesDatasourceSpec'
nullable: true
type: object
display:
$ref: '#/components/schemas/DashboardtypesDisplay'
duration:
@@ -2748,6 +2736,7 @@ components:
links:
items:
$ref: '#/components/schemas/DashboardtypesLink'
nullable: true
type: array
panels:
additionalProperties:
@@ -2764,7 +2753,6 @@ components:
- variables
- panels
- layouts
- links
type: object
DashboardtypesDashboardView:
properties:
@@ -2801,39 +2789,6 @@ components:
required:
- version
type: object
DashboardtypesDatasourcePlugin:
discriminator:
mapping:
signoz/Datasource: '#/components/schemas/DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpec'
propertyName: kind
oneOf:
- $ref: '#/components/schemas/DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpec'
type: object
DashboardtypesDatasourcePluginKind:
enum:
- signoz/Datasource
type: string
DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpec:
properties:
kind:
enum:
- signoz/Datasource
type: string
spec:
$ref: '#/components/schemas/DashboardtypesSigNozDatasourceSpec'
required:
- kind
- spec
type: object
DashboardtypesDatasourceSpec:
properties:
default:
type: boolean
display:
$ref: '#/components/schemas/CommonDisplay'
plugin:
$ref: '#/components/schemas/DashboardtypesDatasourcePlugin'
type: object
DashboardtypesDisplay:
properties:
description:
@@ -3401,6 +3356,7 @@ components:
links:
items:
$ref: '#/components/schemas/DashboardtypesLink'
nullable: true
type: array
plugin:
$ref: '#/components/schemas/DashboardtypesPanelPlugin'
@@ -3412,7 +3368,6 @@ components:
- display
- plugin
- queries
- links
type: object
DashboardtypesPatchOp:
enum:
@@ -3610,8 +3565,6 @@ components:
required:
- queryValue
type: object
DashboardtypesSigNozDatasourceSpec:
type: object
DashboardtypesSource:
enum:
- user
@@ -4256,15 +4209,6 @@ components:
- missingOptionalMetrics
- missingRequiredAttributes
type: object
InframonitoringtypesClusterFilter:
properties:
expression:
type: string
filterByNodeReadiness:
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
filterByPodStatus:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
type: object
InframonitoringtypesClusterRecord:
properties:
clusterCPU:
@@ -4501,13 +4445,6 @@ components:
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesDaemonSetFilter:
properties:
expression:
type: string
filterByPodStatus:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
type: object
InframonitoringtypesDaemonSetRecord:
properties:
currentNodes:
@@ -4580,13 +4517,6 @@ components:
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesDeploymentFilter:
properties:
expression:
type: string
filterByPodStatus:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
type: object
InframonitoringtypesDeploymentRecord:
properties:
availablePods:
@@ -4728,13 +4658,6 @@ components:
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesJobFilter:
properties:
expression:
type: string
filterByPodStatus:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
type: object
InframonitoringtypesJobRecord:
properties:
activePods:
@@ -4858,13 +4781,6 @@ components:
- message
- documentationLink
type: object
InframonitoringtypesNamespaceFilter:
properties:
expression:
type: string
filterByPodStatus:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
type: object
InframonitoringtypesNamespaceRecord:
properties:
counts:
@@ -4946,15 +4862,6 @@ components:
- ready
- notReady
type: object
InframonitoringtypesNodeFilter:
properties:
expression:
type: string
filterByNodeReadiness:
$ref: '#/components/schemas/InframonitoringtypesNodeCondition'
filterByPodStatus:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
type: object
InframonitoringtypesNodeRecord:
properties:
condition:
@@ -5071,13 +4978,6 @@ components:
- shutdown
- unexpectedAdmissionError
type: object
InframonitoringtypesPodFilter:
properties:
expression:
type: string
filterByPodStatus:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
type: object
InframonitoringtypesPodRecord:
properties:
meta:
@@ -5177,7 +5077,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesClusterFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5229,7 +5129,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesDaemonSetFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5255,7 +5155,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesDeploymentFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5307,7 +5207,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesJobFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5333,7 +5233,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesNamespaceFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5359,7 +5259,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesNodeFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5385,7 +5285,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesPodFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5411,7 +5311,7 @@ components:
format: int64
type: integer
filter:
$ref: '#/components/schemas/InframonitoringtypesStatefulSetFilter'
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
@@ -5462,13 +5362,6 @@ components:
- list
- grouped_list
type: string
InframonitoringtypesStatefulSetFilter:
properties:
expression:
type: string
filterByPodStatus:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
type: object
InframonitoringtypesStatefulSetRecord:
properties:
currentPods:

View File

@@ -0,0 +1,11 @@
// Test stub for the Vite-only icon glob module: `import.meta.glob` can't be
// parsed by jest, so every test that transitively imports it is redirected here
// (see moduleNameMapper in jest.config.ts). Provides a minimal name → URL map so
// the resolver stays deterministic under test.
export const ICON_URLS: Record<string, string> = {
'eight-ball': 'mock-eight-ball-url',
};
export const LOGO_URLS: Record<string, string> = {
'aws-dark': 'mock-logo-url',
};

View File

@@ -15,6 +15,9 @@ const config: Config.InitialOptions = {
moduleNameMapper: {
'\\.(png|jpg|jpeg|gif|svg|webp|avif|ico|bmp|tiff)$':
'<rootDir>/__mocks__/fileMock.ts',
// The icon glob module uses `import.meta.glob` (Vite-only); jest can't parse
// it, so redirect any import of it to a stub.
'(^|/)iconAssets$': '<rootDir>/__mocks__/iconAssetsMock.ts',
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
'\\.module\\.mjs$': '<rootDir>/__mocks__/cssMock.ts',

View File

@@ -3236,17 +3236,6 @@ export interface CloudintegrationtypesUpdatableServiceDTO {
config: CloudintegrationtypesServiceConfigDTO;
}
export interface CommonDisplayDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name?: string;
}
export interface CommonJSONRefDTO {
/**
* @type string
@@ -3990,44 +3979,6 @@ export interface DashboardtypesDashboardPanelRefDTO {
panelName: string;
}
export enum DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpecDTOKind {
'signoz/Datasource' = 'signoz/Datasource',
}
export interface DashboardtypesSigNozDatasourceSpecDTO {
[key: string]: unknown;
}
export interface DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpecDTO {
/**
* @enum signoz/Datasource
* @type string
*/
kind: DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpecDTOKind;
spec: DashboardtypesSigNozDatasourceSpecDTO;
}
export type DashboardtypesDatasourcePluginDTO =
DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpecDTO;
export interface DashboardtypesDatasourceSpecDTO {
/**
* @type boolean
*/
default?: boolean;
display?: CommonDisplayDTO;
plugin?: DashboardtypesDatasourcePluginDTO;
}
export type DashboardtypesDashboardSpecDTODatasourcesAnyOf = {
[key: string]: DashboardtypesDatasourceSpecDTO;
};
/**
* @nullable
*/
export type DashboardtypesDashboardSpecDTODatasources =
DashboardtypesDashboardSpecDTODatasourcesAnyOf | null;
export enum DashboardtypesPanelKindDTO {
Panel = 'Panel',
}
@@ -4626,9 +4577,9 @@ export interface DashboardtypesQueryDTO {
export interface DashboardtypesPanelSpecDTO {
display: DashboardtypesDisplayDTO;
/**
* @type array
* @type array,null
*/
links: DashboardtypesLinkDTO[];
links?: DashboardtypesLinkDTO[] | null;
plugin: DashboardtypesPanelPluginDTO;
/**
* @type array
@@ -4807,10 +4758,6 @@ export type DashboardtypesVariableDTO =
| DashboardtypesVariableEnvelopeGithubComSigNozSignozPkgTypesDashboardtypesTextVariableSpecDTO;
export interface DashboardtypesDashboardSpecDTO {
/**
* @type object,null
*/
datasources?: DashboardtypesDashboardSpecDTODatasources;
display: DashboardtypesDisplayDTO;
/**
* @type string
@@ -4821,9 +4768,9 @@ export interface DashboardtypesDashboardSpecDTO {
*/
layouts: DashboardtypesLayoutDTO[];
/**
* @type array
* @type array,null
*/
links: DashboardtypesLinkDTO[];
links?: DashboardtypesLinkDTO[] | null;
/**
* @type object
*/
@@ -4886,9 +4833,6 @@ export interface DashboardtypesDashboardViewDTO {
updatedAt?: string;
}
export enum DashboardtypesDatasourcePluginKindDTO {
'signoz/Datasource' = 'signoz/Datasource',
}
export interface TagtypesGettableTagDTO {
/**
* @type string
@@ -5688,41 +5632,6 @@ export interface InframonitoringtypesChecksDTO {
type: InframonitoringtypesCheckTypeDTO;
}
export enum InframonitoringtypesNodeConditionDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export enum InframonitoringtypesPodStatusDTO {
pending = 'pending',
running = 'running',
failed = 'failed',
unknown = 'unknown',
crashloopbackoff = 'crashloopbackoff',
imagepullbackoff = 'imagepullbackoff',
errimagepull = 'errimagepull',
createcontainerconfigerror = 'createcontainerconfigerror',
containercreating = 'containercreating',
oomkilled = 'oomkilled',
completed = 'completed',
error = 'error',
containercannotrun = 'containercannotrun',
evicted = 'evicted',
nodeaffinity = 'nodeaffinity',
nodelost = 'nodelost',
shutdown = 'shutdown',
unexpectedadmissionerror = 'unexpectedadmissionerror',
no_data = 'no_data',
}
export interface InframonitoringtypesClusterFilterDTO {
/**
* @type string
*/
expression?: string;
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO;
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
}
export type InframonitoringtypesClusterRecordDTOCounts = {
/**
* @type integer
@@ -6100,14 +6009,6 @@ export interface InframonitoringtypesContainersDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesDaemonSetFilterDTO {
/**
* @type string
*/
expression?: string;
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
}
export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6193,14 +6094,6 @@ export interface InframonitoringtypesDaemonSetsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesDeploymentFilterDTO {
/**
* @type string
*/
expression?: string;
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
}
export type InframonitoringtypesDeploymentRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6363,14 +6256,6 @@ export interface InframonitoringtypesHostsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesJobFilterDTO {
/**
* @type string
*/
expression?: string;
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
}
export type InframonitoringtypesJobRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6456,14 +6341,6 @@ export interface InframonitoringtypesJobsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesNamespaceFilterDTO {
/**
* @type string
*/
expression?: string;
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
}
export type InframonitoringtypesNamespaceRecordDTOCounts = {
/**
* @type integer
@@ -6540,15 +6417,11 @@ export interface InframonitoringtypesNamespacesDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesNodeFilterDTO {
/**
* @type string
*/
expression?: string;
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO;
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
export enum InframonitoringtypesNodeConditionDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export type InframonitoringtypesNodeRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6610,14 +6483,6 @@ export interface InframonitoringtypesNodesDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesPodFilterDTO {
/**
* @type string
*/
expression?: string;
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
}
export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6628,6 +6493,27 @@ export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
export type InframonitoringtypesPodRecordDTOMeta =
InframonitoringtypesPodRecordDTOMetaAnyOf | null;
export enum InframonitoringtypesPodStatusDTO {
pending = 'pending',
running = 'running',
failed = 'failed',
unknown = 'unknown',
crashloopbackoff = 'crashloopbackoff',
imagepullbackoff = 'imagepullbackoff',
errimagepull = 'errimagepull',
createcontainerconfigerror = 'createcontainerconfigerror',
containercreating = 'containercreating',
oomkilled = 'oomkilled',
completed = 'completed',
error = 'error',
containercannotrun = 'containercannotrun',
evicted = 'evicted',
nodeaffinity = 'nodeaffinity',
nodelost = 'nodelost',
shutdown = 'shutdown',
unexpectedadmissionerror = 'unexpectedadmissionerror',
no_data = 'no_data',
}
export interface InframonitoringtypesPodRecordDTO {
/**
* @type object,null
@@ -6704,7 +6590,7 @@ export interface InframonitoringtypesPostableClustersDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesClusterFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6758,7 +6644,7 @@ export interface InframonitoringtypesPostableDaemonSetsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesDaemonSetFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6785,7 +6671,7 @@ export interface InframonitoringtypesPostableDeploymentsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesDeploymentFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6839,7 +6725,7 @@ export interface InframonitoringtypesPostableJobsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesJobFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6866,7 +6752,7 @@ export interface InframonitoringtypesPostableNamespacesDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesNamespaceFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6893,7 +6779,7 @@ export interface InframonitoringtypesPostableNodesDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesNodeFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6920,7 +6806,7 @@ export interface InframonitoringtypesPostablePodsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesPodFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6941,21 +6827,13 @@ export interface InframonitoringtypesPostablePodsDTO {
start: number;
}
export interface InframonitoringtypesStatefulSetFilterDTO {
/**
* @type string
*/
expression?: string;
filterByPodStatus?: InframonitoringtypesPodStatusDTO;
}
export interface InframonitoringtypesPostableStatefulSetsDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: InframonitoringtypesStatefulSetFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/

View File

@@ -0,0 +1,7 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.9844 5.0511C7.9844 5.0511 3.17466 6.38853 2.79968 6.90476C2.4247 7.42098 1.77848 8.37718 1.71974 10.4246C1.67224 12.067 2.58844 16.2905 7.09195 16.4305C11.6905 16.5742 13.4029 13.5444 14.2241 11.6683C14.964 9.97584 14.9515 8.0322 15.4203 7.13974C15.889 6.24729 16.3115 5.47358 16.289 4.46488C16.2665 3.45619 15.8902 2.58748 15.2328 2.16501C14.5753 1.74253 14.0128 1.81252 13.7316 1.74253C13.4504 1.67253 7.9844 5.0511 7.9844 5.0511Z" fill="#757E40"/>
<path d="M8.82817 3.36241C6.83328 4.18361 4.18217 4.95732 2.91599 6.74098C1.7723 8.3509 1.39107 10.3533 2.93973 12.6994C4.35341 14.8418 8.12196 15.7555 10.9406 13.7319C13.5217 11.8782 13.7554 9.1571 14.5304 7.3497C15.3053 5.54229 16.9352 3.16742 14.9766 1.99998C12.888 0.757545 10.4043 2.71369 8.82817 3.36241Z" fill="#AFB42A"/>
<path d="M14.5304 2.51733C13.5817 1.96986 12.3717 2.21234 11.0343 2.86981C9.69688 3.52728 8.66444 4.01975 8.19571 4.18349C7.72574 4.34723 3.73345 5.80966 2.96349 7.74955C2.32977 9.34447 2.49476 11.4331 4.15967 12.958C5.82459 14.4829 10.0256 14.3892 11.6443 12.0893C13.2629 9.78945 13.3329 7.93704 13.6854 7.11584C14.0379 6.29463 14.8054 5.09219 15.0229 4.44098C15.2803 3.66727 15.1403 2.86981 14.5304 2.51733Z" fill="#FFF69D"/>
<path d="M9.93064 6.97475C9.17943 6.08355 6.31708 6.36478 5.33214 7.77221C4.34719 9.17963 4.75717 10.3233 5.23839 10.9395C5.82461 11.6908 7.42078 12.3707 8.87445 11.362C10.3281 10.3533 10.7543 7.9522 9.93064 6.97475Z" fill="#855C52"/>
<path d="M6.91449 8.57723C6.54451 9.1197 6.7295 9.99465 6.09328 10.0359C5.45707 10.0772 5.08708 9.05595 5.53831 8.201C6.02579 7.27855 7.18072 6.86732 7.50946 7.3048C7.83944 7.74352 7.26572 8.06226 6.91449 8.57723Z" fill="#D67659"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,47 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7781 9.56835C12.7781 9.56835 16.2542 8.88588 16.4192 10.1983C16.5154 10.972 16.2729 12.3944 14.963 13.5631C13.8756 14.5331 12.0582 15.3493 9.05957 15.2943C6.61595 15.2505 4.95354 14.6643 3.82485 14.0156C1.93495 12.9294 1.56122 11.457 1.65996 10.5333C1.80121 9.20836 3.92484 8.6009 4.02109 8.5459L12.7781 9.56835Z" fill="#F1B34F"/>
<path d="M1.91504 10.8869C1.91504 10.8869 3.91993 14.2142 9.13716 14.2317C14.7856 14.2517 16.5118 10.8781 16.418 10.1969C16.2405 8.89075 13.2307 8.62451 13.2307 8.62451L6.04857 10.3169L1.91504 9.8632V10.8869Z" fill="#FADFB1"/>
<path d="M1.93617 9.68343C1.93617 9.68343 1.61119 10.0734 1.66868 10.8171C1.71993 11.4771 2.3399 12.4495 3.42359 12.8008C4.38229 13.112 5.086 12.7545 6.07845 13.0757C7.0709 13.397 7.98585 14.3719 9.25203 14.2957C10.5182 14.2194 11.1907 13.3145 12.1044 13.182C13.4768 12.9833 14.133 13.352 15.1705 12.3283C16.3142 11.1996 16.3204 10.1484 16.3142 9.88716C16.3017 9.35969 15.9279 9.07471 15.9279 9.07471L3.3311 9.53593L1.93617 9.68343Z" fill="white"/>
<path d="M9.07957 2.68481C4.12483 2.68481 1.64246 5.27593 1.44747 8.23077C1.37248 9.36821 1.74246 10.1132 2.54867 10.8231C3.95609 12.0606 6.52471 12.8355 9.17832 12.8643C12.0219 12.8955 14.1793 12.1756 15.8305 9.99693C16.3692 9.28697 16.4279 8.88324 16.4504 8.09328C16.5192 5.61591 14.2905 2.68481 9.07957 2.68481Z" fill="url(#paint0_radial_721_145)"/>
<path d="M6.53842 7.24459C6.48217 7.12084 6.91715 6.9696 7.13339 6.81211C7.37088 6.63837 7.41462 6.43338 7.50087 6.42213C7.58711 6.41088 7.96584 6.52837 8.93954 6.54087C9.7945 6.55212 10.3357 6.32463 10.4107 6.38963C10.487 6.45463 10.5945 6.63837 10.8332 6.87711C11.0394 7.08334 11.2769 7.22334 11.2882 7.29958C11.2994 7.37583 10.7519 7.49207 10.6557 7.51332C10.5582 7.53457 7.43462 7.52582 7.43462 7.52582C7.43462 7.52582 7.25213 7.47207 7.10089 7.42958C6.94965 7.38458 6.59217 7.36333 6.53842 7.24459Z" fill="#FCDE8C"/>
<path d="M8.97213 7.99091C9.52335 7.99091 9.82458 7.82967 10.1946 7.69842C10.5808 7.56218 10.8983 7.44969 10.8983 7.44969C10.8983 7.44969 10.3246 6.86597 9.05837 6.86597C7.79219 6.86597 7.24097 7.46094 7.24097 7.46094C7.24097 7.46094 8.32341 7.99091 8.97213 7.99091Z" fill="white"/>
<path d="M5.67215 4.62494C5.52341 4.65869 5.51841 4.88618 5.47341 5.10117C5.38217 5.53365 5.58716 5.88363 5.97589 5.77238C6.42086 5.64489 6.31962 5.20866 6.05838 4.91868C5.90589 4.75119 5.82464 4.58994 5.67215 4.62494Z" fill="#F5E5C7"/>
<path d="M7.75965 4.31862C7.71965 4.42611 7.36842 4.49986 7.01094 4.45986C6.74595 4.42986 6.52346 4.16488 6.6647 3.86989C6.80845 3.57116 7.16593 3.53616 7.40841 3.77365C7.65715 4.01738 7.80339 4.19987 7.75965 4.31862Z" fill="#F5E5C7"/>
<path d="M8.52576 4.12493C8.44077 4.10743 8.16203 4.34617 8.10079 4.6124C8.03079 4.90989 8.23078 5.03238 8.39577 5.04988C8.59951 5.07113 8.747 4.91364 8.7595 4.7099C8.7695 4.50741 8.61701 4.14368 8.52576 4.12493Z" fill="#F5E5C7"/>
<path d="M4.20737 7.25711C4.26611 7.39835 4.4911 7.3621 4.71109 7.36835C5.15232 7.38085 5.46105 7.11836 5.28356 6.75463C5.07982 6.3384 4.66734 6.51715 4.42985 6.82463C4.29111 7.00462 4.14737 7.11336 4.20737 7.25711Z" fill="#F5E5C7"/>
<path d="M2.6774 7.23603C2.52491 7.22853 2.46116 7.44727 2.36117 7.64351C2.15993 8.03599 2.26492 8.42846 2.66865 8.42347C3.13113 8.41847 3.14738 7.96974 2.97239 7.62226C2.86989 7.41977 2.83364 7.24228 2.6774 7.23603Z" fill="#F5E5C7"/>
<path d="M14.0117 6.92965C14.163 6.9109 14.1892 6.68466 14.2567 6.47467C14.3892 6.05344 14.2205 5.68471 13.823 5.75721C13.3668 5.8397 13.4255 6.28468 13.6567 6.59841C13.7917 6.7809 13.8567 6.9484 14.0117 6.92965Z" fill="#F5E5C7"/>
<path d="M5.79346 9.21074C5.72096 9.15824 5.49097 9.22698 5.28598 9.30823C4.876 9.47072 4.69601 9.83445 5.00225 10.0969C5.35348 10.3982 5.66221 10.0719 5.75971 9.69571C5.8172 9.47697 5.88095 9.27448 5.79346 9.21074Z" fill="#F5E5C7"/>
<path d="M5.07238 8.79214C5.10738 8.72214 5.01364 8.53215 4.91989 8.38091C4.81115 8.20467 4.6874 8.02468 4.51491 7.97593C3.95994 7.81969 3.75995 8.52715 4.13243 8.75339C4.26868 8.83588 4.41242 8.84088 4.57116 8.84963C4.80865 8.85963 5.03864 8.85963 5.07238 8.79214Z" fill="#F5E5C7"/>
<path d="M3.81117 6.75339C3.78867 6.89463 3.16996 7.01212 2.85372 6.82088C2.47499 6.5909 2.67873 6.06092 3.12996 6.09967C3.51619 6.13467 3.83617 6.5934 3.81117 6.75339Z" fill="#F5E5C7"/>
<path d="M4.13105 5.98216C4.27729 6.02216 4.69977 5.70593 4.82351 5.47594C4.91351 5.30845 4.986 4.89222 4.69352 4.76098C4.36729 4.61474 4.08855 4.82972 4.00606 5.09846C3.90106 5.45344 4.00106 5.94717 4.13105 5.98216Z" fill="#F5E5C7"/>
<path d="M6.631 7.70587C6.65475 7.81462 6.44476 8.40959 5.95478 8.43208C5.6173 8.44708 5.32482 7.99336 5.74604 7.69962C6.06603 7.47838 6.61475 7.62713 6.631 7.70587Z" fill="#F5E5C7"/>
<path d="M7.05344 8.65468C6.9572 8.57343 6.45098 8.93591 6.29848 9.2009C6.20849 9.35839 6.14599 9.64588 6.51847 9.81462C6.87845 9.97836 7.07594 9.70212 7.12094 9.47713C7.16594 9.25215 7.12719 8.71717 7.05344 8.65468Z" fill="#F5E5C7"/>
<path d="M7.56088 10.4421C7.68462 10.6733 8.32584 10.5733 8.47833 10.5271C8.69832 10.4596 9.00955 10.2546 8.81081 9.82835C8.64707 9.47962 8.21959 9.55212 7.96085 9.7721C7.73712 9.96084 7.47588 10.2846 7.56088 10.4421Z" fill="#F5E5C7"/>
<path d="M9.62712 9.62576C9.49463 9.72326 9.59963 10.3195 9.67837 10.4982C9.77962 10.7295 10.1058 10.8195 10.3258 10.6507C10.5796 10.4557 10.5396 10.1332 10.3771 9.95825C10.2533 9.827 9.75587 9.52952 9.62712 9.62576Z" fill="#F5E5C7"/>
<path d="M10.5782 9.25952C10.5295 9.30327 10.0945 9.33326 9.78451 9.06828C9.60577 8.91579 9.61077 8.55455 9.79076 8.42081C9.98825 8.27457 10.2582 8.31957 10.432 8.5733C10.5407 8.72955 10.6345 9.20827 10.5782 9.25952Z" fill="#F5E5C7"/>
<path d="M11.2369 9.04573C11.3244 9.12198 11.8781 9.02823 12.0868 8.787C12.2006 8.6545 12.3631 8.37077 12.1431 8.16828C11.9231 7.96579 11.6643 8.00454 11.4394 8.28077C11.2719 8.48451 11.1469 8.96699 11.2369 9.04573Z" fill="#F5E5C7"/>
<path d="M13.3819 7.3848C13.3394 7.55479 12.7182 7.62479 12.4695 7.50854C12.2158 7.3898 12.2607 7.10357 12.312 6.97982C12.3632 6.85608 12.487 6.70984 12.8132 6.79983C13.1582 6.89483 13.4107 7.27231 13.3819 7.3848Z" fill="#F5E5C7"/>
<path d="M12.5143 9.30959C12.3756 9.27084 12.2943 9.46083 12.2043 9.54583C12.0918 9.65332 11.6194 10.1995 12.2218 10.3395C12.8256 10.4808 12.6606 9.81581 12.6381 9.68082C12.6231 9.58708 12.6156 9.33834 12.5143 9.30959Z" fill="#F5E5C7"/>
<path d="M13.1619 8.95576C13.1119 9.0995 13.3931 9.38324 13.5281 9.54698C13.6631 9.71072 13.9618 9.86196 14.1356 9.53573C14.3106 9.2095 14.0393 9.00701 13.7981 8.95076C13.5569 8.89326 13.2069 8.82577 13.1619 8.95576Z" fill="#F5E5C7"/>
<path d="M15.3192 7.79581C15.3192 7.89206 14.9817 8.16204 14.6042 8.17329C14.323 8.18204 14.1143 8.07955 14.1143 7.80206C14.1143 7.52083 14.368 7.36333 14.683 7.43083C14.8642 7.46833 15.3192 7.64332 15.3192 7.79581Z" fill="#F5E5C7"/>
<path d="M14.6654 6.97962C14.6942 7.09211 15.0317 7.12586 15.2729 7.13211C15.5154 7.13836 15.8129 7.05336 15.7004 6.72088C15.5879 6.3884 15.2554 6.47339 15.1316 6.53464C15.0092 6.59589 14.6279 6.82712 14.6654 6.97962Z" fill="#F5E5C7"/>
<path d="M13.6131 4.7547C13.5119 4.90094 13.7031 5.20468 13.8781 5.33967C14.0531 5.47466 14.2943 5.55966 14.4356 5.32217C14.5768 5.08593 14.3281 4.87219 14.1768 4.77595C14.0243 4.6822 13.7119 4.61221 13.6131 4.7547Z" fill="#F5E5C7"/>
<path d="M13.0444 5.03096C12.9781 4.98971 12.6281 5.07845 12.4257 5.26219C12.2157 5.45218 12.1382 5.72967 12.3919 5.92716C12.6369 6.11715 12.9031 5.93216 13.0106 5.67967C13.1119 5.44218 13.1194 5.0772 13.0444 5.03096Z" fill="#F5E5C7"/>
<path d="M10.3083 4.87363C10.3758 4.77989 10.2521 4.27742 10.0946 4.08493C9.93711 3.89369 9.73962 3.81994 9.52088 3.98368C9.34839 4.11242 9.28465 4.46241 9.53213 4.6424C9.80087 4.83989 10.2346 4.97488 10.3083 4.87363Z" fill="#F5E5C7"/>
<path d="M10.5044 4.09607C10.5882 4.15732 10.8932 4.11357 11.1006 3.97733C11.3081 3.84109 11.3394 3.62485 11.2194 3.47611C11.0731 3.29611 10.8369 3.36361 10.6682 3.5836C10.5207 3.77484 10.3982 4.01733 10.5044 4.09607Z" fill="#F5E5C7"/>
<path d="M11.0059 4.82243C11.0734 5.00242 11.7496 4.96368 11.9458 4.87868C12.1433 4.79369 12.2808 4.61495 12.1708 4.35496C12.0471 4.06247 11.8521 4.06122 11.7096 4.10122C11.4283 4.18122 10.9396 4.64619 11.0059 4.82243Z" fill="#F5E5C7"/>
<defs>
<radialGradient id="paint0_radial_721_145" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.02655 6.51166) rotate(-0.374469) scale(8.19673 5.34427)">
<stop offset="0.1616" stop-color="#F1B14A"/>
<stop offset="0.1688" stop-color="#F1B049"/>
<stop offset="0.3704" stop-color="#E89825"/>
<stop offset="0.5354" stop-color="#E28810"/>
<stop offset="0.6425" stop-color="#E08308"/>
<stop offset="0.7125" stop-color="#E1860D"/>
<stop offset="0.7924" stop-color="#E5901B"/>
<stop offset="0.877" stop-color="#EBA031"/>
<stop offset="0.947" stop-color="#F1B14A"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -0,0 +1,8 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.78857 14.527C3.78857 14.527 6.19095 17.0543 10.312 16.4131C14.433 15.7731 16.0854 12.1596 16.4254 10.0997C16.8579 7.47608 15.6592 5.67242 15.5955 5.57368C15.548 5.50118 3.78857 14.527 3.78857 14.527Z" fill="#DC5819"/>
<path d="M3.60104 3.68857C1.13242 6.28719 0.713693 10.0832 2.56984 13.0168C3.43355 14.3818 3.96227 14.6592 3.96227 14.6592C3.96227 14.6592 8.00081 17.2491 12.5393 13.3231C16.4266 9.96074 15.6029 5.58222 15.6029 5.58222C15.6029 5.58222 15.1179 4.52228 14.098 3.51608C11.8344 1.2812 6.77462 0.346251 3.60104 3.68857Z" fill="#EF6C32"/>
<path d="M13.4956 3.00498C13.4594 3.00748 9.84832 3.26746 6.02852 8.40219C3.61615 11.6445 3.73489 13.9332 3.82864 14.5606C3.82864 14.5606 3.6449 14.4094 3.5149 14.2694C3.38491 14.1306 3.28366 14.0069 3.28366 14.0069C3.27366 12.9082 3.6024 10.8258 5.62854 8.10346C8.71213 3.95493 11.6607 2.87374 12.8969 2.59375C12.8969 2.59375 13.0481 2.6825 13.1944 2.77374C13.3019 2.84124 13.4956 3.00498 13.4956 3.00498Z" fill="#2D3130"/>
<path d="M2.94616 4.47241C3.34738 5.13113 4.31608 6.5548 5.99349 8.04097C8.10463 9.91337 10.2995 11.4358 12.5144 12.5645C13.5606 13.0982 14.3643 13.4107 14.933 13.5944C14.933 13.5944 14.8505 13.7207 14.773 13.8244C14.6955 13.9282 14.6105 14.0157 14.6105 14.0157C14.0318 13.8194 13.2594 13.5069 12.2857 13.0095C10.0333 11.8608 7.8034 10.3146 5.66101 8.4147C4.0736 7.00728 3.11115 5.67485 2.63867 4.92864C2.63867 4.92864 2.70492 4.79739 2.79241 4.68115C2.87991 4.56491 2.94616 4.47241 2.94616 4.47241Z" fill="#2D3130"/>
<path d="M7.64719 1.59619C7.46845 1.79993 7.21346 2.13116 6.91598 2.63239C6.64349 3.08986 6.5035 3.54484 6.34101 4.07231C6.07477 4.93727 5.77353 5.91721 4.78734 7.16965C3.41741 8.90956 2.07123 9.49078 1.44376 9.67702C1.44376 9.67702 1.42626 9.51452 1.42002 9.40078C1.41377 9.28704 1.41627 9.15829 1.41627 9.15829C2.01248 8.95455 3.19242 8.38458 4.39361 6.86091C5.32231 5.68098 5.60854 4.74853 5.86228 3.92607C6.02727 3.3911 6.18226 2.88612 6.486 2.3774C6.62974 2.13616 6.76473 1.92992 6.88848 1.75743C6.88848 1.75743 7.02722 1.71119 7.21721 1.66869C7.3647 1.63369 7.64719 1.59619 7.64719 1.59619Z" fill="#2D3130"/>
<path d="M9.58958 16.5004C9.17085 16.1217 8.30589 15.2568 7.67468 14.0531C6.97596 12.7219 6.64848 10.7195 7.58093 9.46831C8.64588 8.04088 10.6345 7.84714 12.4419 7.84714C12.4444 7.84714 12.4469 7.84714 12.4494 7.84714C14.7506 7.84839 16.4817 8.32962 16.5017 8.33587C16.5017 8.33587 16.488 8.16712 16.4692 8.02338C16.453 7.89964 16.4355 7.78464 16.4355 7.78464C16.348 7.76215 14.8181 7.34842 12.4494 7.34717C12.4469 7.34717 12.4444 7.34717 12.4419 7.34717C10.5108 7.34717 8.37589 7.56466 7.1797 9.16957C6.40974 10.202 6.12726 12.1844 7.2322 14.2856C7.75217 15.2755 8.40714 16.0392 8.88586 16.5204C8.88586 16.5204 9.02586 16.5254 9.2046 16.5229C9.38084 16.5204 9.58958 16.5004 9.58958 16.5004Z" fill="#2D3130"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,11 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.53866 2.43392C1.32492 2.80765 1.6599 3.60261 3.29731 3.92759C5.33596 4.33257 7.53209 3.51261 9.18825 3.80135C10.1307 3.96634 10.7282 4.18008 11.0707 4.33507C11.3369 4.45631 11.3369 4.46631 11.3369 4.46631L11.7594 3.12638C11.7594 3.12638 10.5832 2.48892 9.00576 2.41892C7.42835 2.34893 6.25591 2.86515 4.69224 2.8239C2.33487 2.7614 1.87364 1.8477 1.53866 2.43392Z" fill="#966738"/>
<path d="M13.7792 6.71613L15.0767 6.18616C15.0767 6.18616 15.7491 7.12486 15.9866 8.96726C16.2704 11.1684 15.5816 12.6246 15.8054 13.867C16.0291 15.1094 16.5178 15.7794 16.4753 16.0731C16.4328 16.3669 16.2241 16.6869 15.9454 16.5344C15.6666 16.3806 14.7967 15.2632 14.7592 13.7145C14.7167 11.9971 15.1929 10.5647 14.8017 9.03851C14.3667 7.34735 13.7792 6.71613 13.7792 6.71613Z" fill="#966738"/>
<path d="M11.6594 3.07629L11.0981 4.34873C11.0981 4.34873 11.8494 4.67996 12.6781 5.43492C13.4018 6.09488 13.9205 6.90109 13.9205 6.90109L15.0792 6.18863C15.0792 6.18863 14.478 5.12369 13.5993 4.35998C12.5531 3.45002 11.6594 3.07629 11.6594 3.07629Z" fill="#D29F6C"/>
<path d="M13.8368 3.62756L5.21472 12.4546L5.25222 12.8646L5.6247 12.9208L14.3405 4.11254L13.8368 3.62756Z" fill="#F3C976"/>
<path d="M14.2167 3.2026L13.768 3.70382L14.2605 4.1963L14.7805 3.72132C14.7805 3.72132 15.6079 3.88006 15.6967 3.82757C15.8179 3.75507 16.5441 1.50894 16.5066 1.45019C16.4454 1.35395 15.6867 1.65768 15.2042 1.82892C14.6317 2.03141 14.148 2.2264 14.1042 2.2864C14.0055 2.41889 14.2167 3.2026 14.2167 3.2026Z" fill="#858585"/>
<path d="M15.4192 16.2531C15.3979 16.2531 15.3754 16.2506 15.3542 16.2444L4.93848 13.422C4.85349 13.3983 4.78599 13.3321 4.76349 13.2471L2.10863 3.61756C2.09613 3.57132 2.09613 3.52382 2.11113 3.47757L2.43361 2.41138L2.91109 2.56637L2.6636 3.73006L5.18722 12.9146L15.3192 15.7457L16.0741 14.8745L16.2641 15.3657L15.6129 16.1644C15.5629 16.2219 15.4929 16.2531 15.4192 16.2531Z" fill="#B8CED4"/>
<path d="M5.22598 12.5245L5.6247 12.9208C5.6247 12.9208 5.20348 13.432 5.13598 13.4382C5.09598 13.442 4.74975 13.1583 4.77725 13.0958C4.80475 13.0345 5.22598 12.5245 5.22598 12.5245Z" fill="#5D6265"/>
<path d="M7.46208 10.221C7.45833 10.231 6.79712 10.9272 6.18715 11.5709C5.71468 12.0697 5.2672 12.5534 5.23345 12.5534C5.15721 12.5534 4.46849 11.9034 4.45724 11.7522C4.44599 11.6009 5.49969 10.6922 5.99966 10.3385C7.03211 9.60728 7.46208 10.221 7.46208 10.221Z" fill="#DB0D2A"/>
<path d="M7.85453 10.6246C7.82829 10.6484 7.07083 11.4208 6.44211 12.0621C6.01088 12.502 5.6284 12.8783 5.6234 12.9145C5.61715 12.9658 6.20087 13.6407 6.26212 13.6407C6.37461 13.6407 7.4783 12.4633 7.82079 11.8696C8.32576 10.9946 7.85453 10.6246 7.85453 10.6246Z" fill="#DB0D2A"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,39 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.97592 1.44005C6.96717 1.62005 6.80718 16.5155 6.82593 16.5918C6.84468 16.668 7.93962 16.6468 7.93962 16.603C7.93962 16.5068 8.13336 1.5488 8.12961 1.4513C8.12586 1.39131 6.97842 1.37256 6.97592 1.44005Z" fill="#FFD51D"/>
<path d="M6.97341 1.55371C6.97341 1.55371 7.35339 2.06868 7.59088 2.37742C7.80337 2.65365 8.1121 3.00363 8.1121 3.00363L8.1021 3.75359C8.1021 3.75359 7.62213 3.21612 7.37964 2.92739C7.11715 2.61615 6.96466 2.36867 6.96466 2.36867L6.97341 1.55371Z" fill="#FDA726"/>
<path d="M6.93969 4.33862C6.93969 4.33862 7.18218 4.76485 7.49716 5.16858C7.7534 5.49731 8.07338 5.79355 8.07338 5.79355L8.05713 7.27597L6.90344 7.05973L6.93969 4.33862Z" fill="#FDA726"/>
<path d="M6.86471 11.6094L6.86096 12.1306C6.86096 12.1306 7.09095 12.5144 7.38343 12.8468C7.62717 13.1231 7.9784 13.4968 7.9784 13.4968L8.00215 11.6094L7.52968 11.3782L6.86471 11.6094Z" fill="#FDA726"/>
<path d="M6.84093 14.1719C6.84093 14.1719 7.20967 14.6531 7.4659 14.9406C7.66964 15.1693 7.95463 15.4693 7.95463 15.4693L7.94463 16.1855C7.94463 16.1855 7.4659 15.7018 7.24966 15.4693C7.03342 15.2368 6.83344 15.0043 6.83344 15.0043L6.84093 14.1719Z" fill="#FDA726"/>
<path d="M2.10621 13.4207C2.10621 13.4207 1.91872 13.1407 2.03371 12.782C2.15496 12.402 2.42994 12.262 2.55869 11.9583C2.73618 11.5383 2.69493 11.1983 3.02741 10.8833C3.32615 10.6008 3.73862 10.7058 4.10236 10.6008C4.46609 10.4958 4.61983 10.2771 4.61983 10.2771L5.46853 10.5358C5.46853 10.5358 5.35604 11.4808 5.01606 11.4645C4.67608 11.4483 3.50489 11.667 3.48114 11.7145C3.45739 11.7633 2.41494 13.4032 2.41494 13.4032L2.10621 13.4207Z" fill="#8A867E"/>
<path d="M9.99951 11.3682C10.0158 11.4407 10.2907 11.8369 10.727 12.1919C11.1632 12.5469 11.8094 12.8056 12.0282 12.9681C12.2469 13.1294 12.4081 13.2019 12.6744 13.5581C12.9406 13.9143 13.0294 14.1318 13.2243 14.4068C13.4181 14.6818 13.6206 14.8755 13.7981 15.013C13.9818 15.1543 14.3555 15.3767 14.4205 15.368C14.4855 15.3605 14.768 14.7705 14.768 14.7705C14.768 14.7705 13.9143 13.7981 13.7231 13.4681C13.5318 13.1394 13.1493 12.6181 12.7244 12.3744C12.5119 12.2519 11.1232 10.5208 11.1232 10.5208L9.99951 11.3682Z" fill="#8A867E"/>
<path d="M12.6732 7.68727C12.6732 7.68727 12.8119 7.52478 12.9019 7.46228C13.0156 7.38228 13.1081 7.33604 13.2881 7.33229C13.4506 7.32979 13.9506 7.76476 13.9506 7.76476C13.9506 7.76476 15.133 9.9559 15.1368 9.95215C15.1405 9.9484 15.9055 10.8246 15.8917 10.8071C15.878 10.7884 16.3617 11.0371 16.3617 11.0371C16.3617 11.0371 16.2792 11.5821 15.3143 11.5533C14.3706 11.5258 13.7743 10.9896 13.4394 10.2771C13.2781 9.93465 13.2531 9.31343 13.2531 9.31343L12.6732 7.68727Z" fill="#7A57BD"/>
<path d="M13.0581 7.3709C13.0581 7.3709 13.1256 7.67214 13.3706 7.88963C13.6156 8.10587 13.8306 8.29586 14.0006 8.56834C14.1705 8.84083 14.5543 9.83452 15.0468 10.4145C15.5392 10.9945 15.7255 11.032 15.9167 11.102C16.0779 11.1607 16.2342 11.1545 16.2917 11.132C16.3804 11.0995 16.4642 10.927 16.0592 10.5532C15.6542 10.1795 15.4005 9.73703 15.178 9.29205C14.9555 8.84708 14.8443 7.85338 14.233 7.4409C13.6218 7.02967 13.0581 7.3709 13.0581 7.3709Z" fill="#A47FCC"/>
<path d="M13.0418 7.92847C13.0418 7.92847 13.4293 8.43219 13.4131 9.12215C13.3968 9.81212 13.0606 10.3008 13.0606 10.7096C13.0606 11.1183 13.1493 11.3995 13.5493 11.5195C13.9505 11.6395 14.1668 11.712 14.2393 11.8808C14.3118 12.0495 14.2955 12.4745 14.4793 12.827C14.663 13.1794 14.803 13.2607 14.888 13.4444C14.9842 13.6494 15.0005 13.9894 15.0005 13.9894C15.0005 13.9894 14.8005 14.9593 14.568 14.5506C14.3355 14.1419 13.5331 12.482 13.3093 12.2895C13.0843 12.097 11.8819 10.7733 11.8819 10.7021C11.8819 10.6308 13.0418 7.92847 13.0418 7.92847Z" fill="#ACA399"/>
<path d="M2.77615 13.5869L3.23113 13.6507L3.25238 13.322C3.25238 13.322 3.40112 13.0145 3.54861 12.8132C3.69735 12.612 3.83485 12.3895 4.11983 12.347C4.40607 12.3045 6.21597 12.347 6.36471 12.262C6.51346 12.177 6.81969 11.7958 6.81969 11.7958C6.81969 11.7958 7.91338 11.8908 9.04332 11.6583C9.84828 11.492 10.377 11.4358 10.377 11.4358L11.1707 11.3621C11.1707 11.3621 12.0069 10.9921 11.9857 10.9171C11.9644 10.8433 11.3395 9.70964 11.2757 9.74214C11.212 9.77339 6.43721 10.2184 6.36346 10.3034C6.28972 10.3884 5.42101 11.2771 5.42101 11.2771C5.42101 11.2771 3.51486 11.7745 3.48362 11.7858C3.45237 11.7958 2.65741 13.4482 2.65741 13.4482L2.77615 13.5869Z" fill="#ACA399"/>
<path d="M4.02607 9.23706C4.02607 9.23706 4.02107 8.84833 4.08232 8.36336C4.14106 7.89338 4.24981 7.37216 4.26106 6.96968C4.27356 6.52095 4.22231 6.25472 4.17481 6.23972C4.12857 6.22472 3.90233 6.75969 3.90233 6.75969C3.90233 6.75969 3.91608 7.00218 3.81233 7.24092C3.68984 7.52215 3.49485 7.42216 3.49485 7.42216L3.26361 6.6372L3.71484 5.39101L3.95607 3.6936L4.75853 4.26232L5.296 6.40471L5.10851 8.4371L4.56979 9.44955L4.02607 9.23706Z" fill="#ACA399"/>
<path d="M11.2245 7.05347L10.6982 8.3184L11.287 11.0195L10.7832 11.412C10.7832 11.412 11.1532 12.1219 11.7294 12.4019C12.3069 12.6819 12.8206 12.6369 12.9919 12.6269C13.1631 12.6182 13.2781 12.6919 13.3794 12.8344C13.8593 13.5031 13.8306 14.1093 13.9643 14.3581C14.1981 14.7906 14.488 14.8268 14.488 14.8268C14.488 14.8268 15.0105 14.6368 14.993 14.6018C14.9755 14.5656 14.7493 14.1593 14.7493 14.1593C14.7493 14.1593 14.6593 13.7631 14.4518 13.4019C14.2443 13.0407 14.1268 12.8607 14.0731 12.4819C14.0193 12.1032 14.0006 11.9232 13.5956 11.8332C13.1894 11.7432 12.6669 11.4907 12.7031 10.977C12.7394 10.4633 13.3844 9.37084 13.2894 8.57839C13.2081 7.90217 12.6219 7.26221 11.7019 7.11721C11.4482 7.07722 11.2245 7.05347 11.2245 7.05347Z" fill="#F1EDEC"/>
<path d="M15.0118 13.7907C14.9555 13.8007 14.813 14.1419 14.7055 14.3231C14.6206 14.4656 14.3993 14.7919 14.4168 14.8369C14.4343 14.8819 15.1393 15.5631 15.183 15.5943C15.2468 15.6393 15.373 15.5943 15.4268 15.3956C15.4805 15.1968 15.743 14.0607 15.743 14.0607C15.743 14.0607 15.1205 13.7719 15.0118 13.7907Z" fill="#5E6367"/>
<path d="M11.8107 7.56722C11.7644 7.70471 11.8919 7.80221 12.1169 8.01845C12.3419 8.23468 12.4782 8.49592 12.5044 8.8034C12.5319 9.10964 12.5831 9.33713 12.7481 9.34463C12.9469 9.35337 13.1669 8.8934 12.8294 8.20843C12.4694 7.47722 11.8382 7.48597 11.8107 7.56722Z" fill="white"/>
<path d="M11.2957 7.2373L11.2782 10.9621C11.2782 10.9621 10.967 11.0834 10.577 10.9621C10.187 10.8409 10.057 10.5984 10.057 10.5984C10.057 10.5984 9.77955 11.0659 9.02584 11.0571C8.27213 11.0484 8.08214 10.6934 8.08214 10.6934C8.08214 10.6934 7.95089 10.9859 7.39842 11.0571C7.08594 11.0971 6.8197 11.0971 6.7672 10.9759C6.71471 10.8546 5.841 8.14476 5.841 8.14476L8.80085 7.37605L11.2957 7.2373Z" fill="#B0E4FE"/>
<path d="M7.16468 8.23345L7.13843 10.6058C7.13843 10.6058 7.39716 10.6083 7.63215 10.4496C7.87714 10.2833 7.98713 10.1171 8.03088 10.1209C8.09088 10.1259 8.40336 10.6071 9.00083 10.6233C9.66829 10.6408 9.96203 10.0696 10.0145 10.0696C10.0833 10.0696 10.2295 10.3171 10.3783 10.3983C10.647 10.5458 10.8457 10.4758 10.8457 10.4758L10.8545 7.78223L7.16468 8.23345Z" fill="#34B6E2"/>
<path d="M2.01994 4.46994C2.01994 4.46994 1.9012 3.72873 2.17744 3.29375C2.45367 2.85877 2.87865 2.74003 2.87865 2.74003L3.65986 2.54254C3.65986 2.54254 3.96234 2.41629 4.53981 2.43379C5.10978 2.45129 5.59851 2.74378 5.86349 2.89877C6.16973 3.07626 6.45596 3.46249 6.35722 3.62998C6.25847 3.79747 5.99223 3.67998 5.99223 3.67998C5.99223 3.67998 6.71345 4.1837 7.05968 5.0924C7.34716 5.84861 7.38591 6.61357 7.38591 6.61357L8.31461 8.066L11.1207 6.68232C11.1207 6.68232 11.3095 6.67107 11.4269 6.76107C11.5944 6.88981 11.6257 7.0948 11.6257 7.0948C11.6257 7.0948 11.4444 7.14479 11.2482 7.42353C10.7045 8.19474 10.3883 10.0521 8.88708 9.9234C7.58215 9.81215 7.35591 8.50097 7.26592 8.16474C7.17717 7.82851 6.00848 6.75732 6.00848 6.75732L4.89479 4.21245L3.7186 3.13501L2.44367 4.38994L2.01994 4.46994Z" fill="#7A57BD"/>
<path d="M4.64982 10.9221C4.64982 10.9221 4.39733 11.0658 4.07235 11.1121C3.74736 11.1571 3.40488 11.1033 3.14365 11.3821C2.88241 11.6608 2.9899 12.0583 2.78241 12.3645C2.57493 12.6708 2.40368 12.8607 2.23244 13.0495C2.0612 13.2382 2.02495 13.4557 2.0337 13.6357C2.04245 13.8157 2.1962 14.1319 2.1962 14.1319L3.0624 14.0682L3.05365 13.5095C3.05365 13.5095 2.93741 13.3082 3.09865 13.032C3.20239 12.8545 3.48613 12.3833 3.57612 12.2658C3.66612 12.1483 3.72612 12.0108 3.93735 11.977C4.16609 11.9395 5.9935 11.9233 6.16474 11.8421C6.33598 11.7608 6.70596 11.3646 6.73346 11.0671C6.76096 10.7696 6.77845 8.35224 6.77845 8.35224L6.9472 7.02106C6.9472 7.02106 6.84845 6.75107 6.74721 6.47983C6.40472 5.55988 5.98475 4.59743 5.18979 3.68248C4.40858 2.78378 3.62237 2.87877 3.62237 2.87877C3.62237 2.87877 2.40493 1.77008 2.34119 1.67884C2.27744 1.58759 2.1887 1.49885 2.16995 1.57009C2.1512 1.64134 2.2062 2.41755 2.2062 2.41755L2.65742 3.19251C2.65742 3.19251 2.56743 3.56249 2.41368 3.93247C2.25994 4.30245 2.01996 4.46869 2.01996 4.46869L1.92871 6.1086C1.92871 6.1086 1.73747 6.40734 1.73122 6.79607C1.72497 7.17855 3.24989 6.75607 3.25614 6.72357C3.26239 6.69107 3.77736 5.28865 3.77111 5.2174C3.76486 5.14616 4.15484 4.02371 4.15484 4.02371C4.15484 4.02371 4.84731 5.01866 4.73731 6.17485C4.69981 6.57483 4.63357 6.99856 4.57732 7.33979C4.39358 8.46223 4.25859 9.21844 4.25859 9.21844L4.64982 10.6221V10.9221Z" fill="#F1EDEC"/>
<path d="M3.60862 13.8983C3.60862 13.952 3.44488 14.232 3.1024 14.692C2.75992 15.1519 2.63742 15.3019 2.50868 15.1194C2.37994 14.9369 2.33244 14.627 2.28369 14.4632C2.23994 14.3157 2.1662 14.0782 2.1662 14.0782L3.25239 13.332C3.25364 13.332 3.60862 13.7808 3.60862 13.8983Z" fill="#5E6367"/>
<path d="M2.79364 5.25611C2.73739 5.2811 2.6874 5.52984 2.88239 5.77483C3.07738 6.01981 3.30111 6.13731 3.30111 6.13731L3.47361 5.75858C3.47361 5.75858 3.23487 5.60859 3.09987 5.48484C2.96113 5.3561 2.84364 5.23361 2.79364 5.25611Z" fill="white"/>
<path d="M2.16995 1.56992C2.13496 1.58617 1.92622 1.85616 2.06996 2.40238C2.15495 2.72611 2.76242 3.32608 2.76242 3.32608L3.1249 3.57982C3.1249 3.57982 3.07116 3.20234 2.85492 2.9486C2.63868 2.69486 2.38494 2.40988 2.3162 2.17114C2.24745 1.93241 2.2862 1.51618 2.16995 1.56992Z" fill="#ACA399"/>
<path d="M1.92498 6.10737C1.92498 6.10737 1.71374 6.4811 1.72249 6.84483C1.72999 7.17731 1.83624 7.35605 2.04123 7.53729C2.24497 7.71728 2.59745 7.84853 2.81744 7.75978C2.84743 7.74728 2.85993 7.53729 2.93118 7.38105C2.98618 7.26231 3.17742 7.07107 3.17742 7.07107C3.17742 7.07107 3.26991 7.27106 3.28491 7.37105C3.29991 7.47105 3.30991 7.66979 3.34616 7.67854C3.4849 7.71353 3.67614 7.57479 3.79738 7.27481C3.92738 6.95357 3.90113 6.76608 3.90113 6.76608L4.10862 6.29111C4.10862 6.29111 4.00112 5.4599 3.29241 5.26741C2.5837 5.07492 1.96373 5.4299 1.96373 5.4299L1.92498 6.10737Z" fill="url(#paint0_radial_715_295)"/>
<path d="M2.22246 6.3922C2.02497 6.36971 1.94623 6.59094 1.94623 6.83718C1.94623 7.08342 2.07872 7.18591 2.20496 7.18591C2.33121 7.18591 2.4462 7.02342 2.4462 6.79468C2.44495 6.5672 2.37871 6.41095 2.22246 6.3922Z" fill="#845B51"/>
<path d="M2.35742 4.74364C2.37992 4.79989 2.44742 4.88989 2.54366 4.92363C2.63991 4.95738 2.7524 4.94113 2.7524 4.94113C2.7524 4.94113 2.78115 5.18362 2.95489 5.18987C3.13738 5.19612 3.15238 4.87364 3.12363 4.71615C3.09488 4.55865 2.99989 4.45116 2.99989 4.45116C2.99989 4.45116 2.7124 4.41116 2.53116 4.53616C2.39867 4.62615 2.35742 4.74364 2.35742 4.74364Z" fill="#464D50"/>
<path d="M3.26488 2.55373C3.26488 2.55373 3.56736 2.79622 3.60486 3.21495C3.64611 3.68367 3.64486 3.78992 3.64486 3.78992L2.54367 3.09745C2.54367 3.09745 2.49117 3.2187 2.48867 3.35369C2.48617 3.49993 2.50742 3.65118 2.50742 3.65118L3.65111 4.35364C3.65111 4.35364 3.60736 4.97986 3.42237 5.63482C3.25863 6.21229 3.03364 6.64227 2.99614 6.72226C2.95864 6.80226 2.8274 7.14474 3.13863 7.22224C3.41112 7.28973 3.46612 6.901 3.54611 6.72726C3.62611 6.55352 3.81985 6.0648 3.93484 5.63357C4.24983 4.45363 4.18858 3.74867 4.11983 3.2612C4.04734 2.73997 3.88734 2.63998 3.69611 2.55873C3.55861 2.50249 3.35612 2.46499 3.26488 2.55373Z" fill="#34B6E2"/>
<path d="M4.01483 8.99843C4.01483 8.99843 3.98609 9.34841 4.06233 9.87089C4.12358 10.2846 4.44356 10.9633 4.6548 10.9121C4.70605 10.8996 4.57356 10.2534 4.77729 9.99588C4.91604 9.81964 5.16852 9.78839 5.81724 9.42091C6.35971 9.11343 6.84094 8.51221 6.84094 8.51221L6.73969 7.62476L6.05598 7.79225C6.05598 7.79225 5.556 8.27597 4.96978 8.5922C4.44856 8.87469 4.01483 8.99843 4.01483 8.99843Z" fill="#D853C9"/>
<path d="M6.45474 7.58598C6.45099 7.58723 6.211 7.48974 6.05601 7.74973C5.86477 8.07096 5.86352 8.65218 5.84477 8.69592C5.82602 8.73967 5.70978 8.87842 5.82602 8.90216C5.94101 8.92591 6.38849 8.88716 6.67598 8.68093C6.96471 8.47469 7.01721 8.19095 7.01721 8.19095L6.45474 7.58598Z" fill="#34B6E2"/>
<path d="M11.2569 6.6974C11.2569 6.6974 11.0832 6.68865 10.6982 7.17862C10.1758 7.84233 9.90827 9.20726 8.97582 9.20726C8.04337 9.20726 7.89463 8.3948 7.64839 7.76734C7.40215 7.13987 6.94718 6.79739 6.94718 6.79739C6.94718 6.79739 6.70844 6.64865 6.83593 6.45491C6.93343 6.30617 7.25091 6.43991 7.42965 6.5224C7.60839 6.6049 7.93088 6.81364 8.13336 6.89363C8.69209 7.11487 9.20831 6.97738 9.49954 6.91113C9.79078 6.84364 10.0608 6.71864 10.4995 6.6724C10.9895 6.61865 11.2569 6.6974 11.2569 6.6974Z" fill="#A47FCC"/>
<path d="M6.66972 6.9986C6.42599 7.16734 6.1635 7.55357 6.46848 8.04729C6.74722 8.49727 7.33969 8.35353 7.50968 8.18353C7.72342 7.9698 7.80841 7.55607 7.58593 7.19859C7.36344 6.84111 6.89721 6.83986 6.66972 6.9986Z" fill="#FFA6A3"/>
<path d="M6.94471 7.32231C6.81596 7.35481 6.69347 7.53855 6.75846 7.73604C6.81471 7.90978 7.01845 7.98103 7.14844 7.91853C7.28594 7.85228 7.37343 7.66729 7.29594 7.5023C7.23219 7.36356 7.09345 7.28481 6.94471 7.32231Z" fill="#F182A7"/>
<defs>
<radialGradient id="paint0_radial_715_295" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(2.61485 7.97649) scale(2.18616)">
<stop offset="0.6623" stop-color="#FFA8A6"/>
<stop offset="0.9142" stop-color="#FFA8A6" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,17 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.02217 9.55581L1.83744 9.7433C1.83744 9.7433 1.50871 9.41456 1.55621 9.29707C1.6037 9.17958 5.89723 5.82475 6.06097 5.8485C6.22471 5.87225 6.60094 6.27098 6.90592 6.12974C7.21091 5.98849 6.90592 5.16729 6.99967 5.05104C7.09341 4.93355 10.4482 3.12739 10.6595 3.03365C10.8707 2.9399 11.0582 2.72867 11.4807 2.82241C11.9032 2.91616 12.9594 3.10365 14.108 3.8786C15.2567 4.65356 16.4542 5.54477 16.3604 6.03724C16.2667 6.52972 7.02217 9.55581 7.02217 9.55581Z" fill="#FFE265"/>
<path d="M3.43233 15.4455C4.58227 15.3042 15.3042 13.0056 15.7504 12.9356C16.1967 12.8656 16.4779 12.4431 16.4779 11.9732C16.4779 11.5032 16.3604 6.03723 16.3604 6.03723C16.3604 6.03723 15.8442 6.20097 15.2342 6.34222C14.6242 6.48221 9.62576 7.70214 7.65586 8.14837C5.68596 8.5946 2.54113 9.4158 2.18865 9.4158C1.83617 9.4158 1.55493 9.29831 1.55493 9.29831C1.55493 9.29831 1.64868 14.1781 1.67243 14.5305C1.69617 14.883 1.85992 15.3042 2.25864 15.4217C2.65737 15.5392 2.91736 15.508 3.43233 15.4455Z" fill="#FEB502"/>
<path d="M10.3708 4.19483C10.3708 4.49482 10.6758 4.69356 11.1808 4.67856C11.732 4.66231 11.8845 4.33483 11.867 4.12484C11.8495 3.9136 11.5958 3.72861 11.0933 3.73736C10.6271 3.74611 10.3708 3.9311 10.3708 4.19483Z" fill="#FF8B0D"/>
<path d="M4.73988 7.93458C3.92742 8.20832 4.01867 8.91953 4.25616 9.07827C4.49364 9.23701 4.80988 9.22826 5.17986 9.09577C5.54984 8.96328 5.78483 8.81203 5.82232 8.56455C5.85732 8.32956 5.54984 7.66084 4.73988 7.93458Z" fill="#ED6B31"/>
<path d="M12.122 5.90225C11.5771 5.92725 10.9746 6.31848 10.7496 6.69471C10.5121 7.09094 10.4946 7.55716 11.0396 7.84715C11.5846 8.13713 13.512 7.39842 13.4157 6.72096C13.3195 6.04224 12.6945 5.876 12.122 5.90225Z" fill="#ED6B31"/>
<path d="M10.2307 11.3832C10.2307 11.3832 11.0045 10.8107 11.0045 10.407C11.0045 10.002 10.6707 9.04583 9.25329 8.97334C7.92461 8.90459 7.10591 10.7157 7.97711 11.6482C8.84832 12.5806 9.37579 12.1582 9.70202 12.0707C10.0283 11.9819 10.2307 11.3832 10.2307 11.3832Z" fill="#ED6B31"/>
<path d="M6.40339 13.0106C6.40339 13.0106 7.17209 12.5294 6.58213 12.0019C6.16715 11.6307 5.61218 11.7982 5.32969 12.0869C4.96846 12.4569 5.06596 13.0456 5.39969 13.3093C5.73467 13.5743 6.40339 13.0106 6.40339 13.0106Z" fill="#ED6B31"/>
<path d="M3.79857 10.8469C3.79857 10.8469 3.71108 10.5707 3.39734 10.4594C3.04486 10.3345 2.51864 10.7732 2.74363 11.2419C2.94612 11.6644 3.39484 11.5406 3.39484 11.5406L3.79857 10.8469Z" fill="#ED6B31"/>
<path d="M15.0518 11.2507C15.1281 11.1682 15.6268 10.8083 15.6268 10.5345C15.6268 10.262 14.8756 9.41958 13.9169 10.0533C12.9582 10.687 13.4594 11.6457 13.7232 11.8045C13.9869 11.9632 14.7181 11.612 15.0518 11.2507Z" fill="#ED6B31"/>
<path d="M4.85735 8.36585C4.32363 8.55084 4.12239 8.92832 4.25613 9.07831C4.39737 9.23705 4.70486 9.34205 5.24108 9.14831C5.65606 8.99831 5.86605 8.74708 5.83105 8.52334C5.80605 8.36085 5.34108 8.19836 4.85735 8.36585Z" fill="#FF8B0D"/>
<path d="M3.07736 10.8282C2.84362 11.1207 3.03361 11.4532 3.2536 11.5319C3.47359 11.6107 3.86106 11.3732 3.83482 10.9782C3.80732 10.582 3.2886 10.5645 3.07736 10.8282Z" fill="#FF8B0D"/>
<path d="M5.79599 12.4219C5.29352 12.8294 5.27977 13.2281 5.47351 13.3631C5.6085 13.4581 6.10597 13.6556 6.56095 13.3194C6.89843 13.0706 6.90468 12.5482 6.79844 12.2982C6.70969 12.0857 6.21847 12.0782 5.79599 12.4219Z" fill="#FF8B0D"/>
<path d="M8.84581 10.2007C8.17334 10.8544 8.01335 11.9156 8.67332 12.1656C9.25454 12.3856 10.0895 12.2531 10.582 11.6469C11.0994 11.0094 11.1544 10.3057 10.822 9.89072C10.4245 9.392 9.36578 9.69698 8.84581 10.2007Z" fill="#FF8B0D"/>
<path d="M11.9019 6.55347C11.022 6.85221 10.5033 7.48592 10.952 7.80341C11.4007 8.11964 11.8669 8.07339 12.3769 7.89965C12.9556 7.70341 13.3981 7.24844 13.4156 6.81721C13.4294 6.46348 12.6769 6.28974 11.9019 6.55347Z" fill="#FF8B0D"/>
<path d="M14.2518 10.5995C13.6531 11.087 13.4781 11.5757 13.7243 11.8044C13.9706 12.0332 14.5606 12.1382 15.1055 11.7607C15.6717 11.3682 15.713 10.9232 15.6505 10.5645C15.5805 10.1608 14.8668 10.0983 14.2518 10.5995Z" fill="#FF8B0D"/>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,42 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.99861 5.46985L2.46614 5.59234C2.46614 5.59234 1.72742 6.73228 1.56618 7.94472C1.46494 8.70593 1.46119 9.69962 1.53119 10.1946C1.60243 10.6896 1.85742 11.7433 2.25865 12.4857C2.64488 13.2007 3.02111 13.4819 3.43233 13.7307C3.60858 13.8369 3.76607 14.2006 4.04105 14.3769C4.29854 14.5419 4.63852 14.5744 4.63852 14.5744C4.63852 14.5744 5.57847 16.0505 7.61211 16.3068C9.99824 16.6068 11.1657 15.5293 11.1657 15.5293C11.1657 15.5293 12.1206 15.5268 13.0218 15.1406C14.033 14.7069 14.573 13.8044 14.573 13.8044C14.573 13.8044 15.8392 12.7145 16.2404 11.4095C16.5629 10.3621 16.5716 8.37094 16.3242 7.43349C16.0767 6.49604 15.5979 5.70233 15.218 5.19361C14.9155 4.78863 11.5382 2.99373 11.1307 2.83373C10.7245 2.67499 6.9409 2.42751 6.79966 2.53375C6.65841 2.64125 2.99861 5.46985 2.99861 5.46985Z" fill="#AC5811"/>
<path d="M4.31614 3.46869C3.43494 4.18615 2.97871 4.56113 2.52124 5.45858C2.06376 6.35603 2.21125 7.54722 2.29375 7.9872C2.375 8.42718 2.52249 9.27588 2.60373 9.69961C2.68498 10.1246 2.90122 11.132 3.49994 11.7558C4.33864 12.6295 5.0986 12.9307 5.0986 12.9307C5.0986 12.9307 5.71482 13.8856 7.20349 14.4319C9.16089 15.1493 10.0421 14.1056 10.7921 13.8281C11.542 13.5507 12.4395 13.5344 13.2069 12.9957C13.9731 12.457 14.8218 11.8045 15.3768 10.4346C15.9318 9.06464 15.8168 8.46092 15.833 8.11844C15.8493 7.77596 15.8005 6.12855 15.1643 5.11735C14.5281 4.10615 13.7631 3.49868 13.4682 3.30619C12.8157 2.88247 10.4858 1.86877 9.8996 1.75503C9.31213 1.64128 7.21724 1.81877 6.38603 2.16125C5.55483 2.50374 4.31614 3.46869 4.31614 3.46869Z" fill="url(#paint0_radial_718_89)"/>
<path d="M8.03476 9.93212C7.89601 9.88837 7.74977 9.68213 7.71477 9.2409C7.66602 8.61219 8.12975 8.43845 8.12975 8.43845C8.12975 8.43845 8.68222 8.2922 9.07095 8.15346C9.43218 8.02472 9.69217 7.85973 9.69217 7.85973C9.69217 7.85973 9.72717 7.18601 9.9509 6.98727C10.1759 6.78853 10.5721 6.80603 11.0133 6.85728C11.4533 6.90853 12.162 7.20226 12.0583 7.46975C11.9545 7.73723 11.8333 8.00472 11.4108 8.24721C10.9883 8.48844 10.6859 8.71343 10.4696 8.90342C10.2534 9.09466 9.85716 9.33465 9.20094 9.57714C8.54473 9.81962 8.23474 9.99461 8.03476 9.93212Z" fill="#FFC86E"/>
<path d="M4.31864 4.30862C3.90366 4.22363 3.31744 5.12233 3.2087 5.41981C3.0987 5.7173 2.98621 6.44226 2.97371 6.77349C2.95746 7.18472 2.94996 7.57095 3.12245 7.88468C3.25495 8.12717 3.57618 8.26341 3.86491 8.01343C4.15365 7.76344 4.65487 7.07848 4.78736 6.98223C4.91986 6.88599 5.30734 6.75849 5.15485 6.12853C5.09235 5.87104 4.45863 5.33857 4.42738 5.26607C4.39739 5.19483 4.55363 4.35737 4.31864 4.30862Z" fill="#FFC86E"/>
<path d="M7.25225 3.17991C7.2685 3.42115 7.46599 3.80738 7.67973 4.01611C7.99971 4.32735 9.46713 5.37979 9.84712 5.3198C10.2271 5.2598 10.6408 4.57733 10.7621 4.38734C10.8833 4.19736 11.1245 3.92987 11.2195 3.80863C11.3145 3.68738 11.5645 3.41115 11.3583 3.13491C11.1508 2.85868 10.6333 2.67744 10.2883 2.65119C9.94336 2.62494 9.40714 2.4012 8.94966 2.4187C8.49219 2.4362 7.20975 2.55744 7.25225 3.17991Z" fill="#FFC86E"/>
<path d="M13.292 6.39853C13.3208 6.66102 13.6895 6.71851 13.9832 6.9085C14.277 7.09849 14.4757 7.29723 14.6482 7.24474C14.8207 7.19349 15.0557 6.91725 15.0019 6.43353C14.9332 5.81231 14.5444 5.01735 14.0007 5.0436C13.672 5.05985 13.717 5.63107 13.717 5.63107C13.717 5.63107 13.2745 6.24354 13.292 6.39853Z" fill="#FFC86E"/>
<path d="M6.1836 4.77845C6.1836 4.77845 5.00491 4.77095 5.02616 4.88844C5.04866 5.00594 5.15116 5.39342 5.45864 5.40842C5.76612 5.42341 6.16235 5.45216 6.35234 5.39342C6.54233 5.33467 7.06981 4.90969 7.06981 4.7197C7.06981 4.52971 6.85732 4.25098 6.85732 4.25098L6.1836 4.77845Z" fill="#CF701E"/>
<path d="M7.86715 6.93968C7.86715 6.93968 7.8534 6.94842 7.8334 6.96467C7.77466 7.01342 7.66216 7.12717 7.70591 7.25466C7.76466 7.4234 8.10839 7.73838 8.44587 7.73838C8.78335 7.73838 8.96584 7.4234 9.02459 7.21841C9.08334 7.01342 9.05334 6.88843 9.05334 6.88843L7.86715 6.93968Z" fill="#CF701E"/>
<path d="M5.23871 7.89969C5.23871 7.89969 5.03372 7.97344 5.04122 8.10468C5.04872 8.23593 5.09996 8.47841 5.3637 8.73465C5.62994 8.99339 6.01616 9.06463 6.3674 8.96964C6.71863 8.87464 6.97486 8.61091 6.94612 8.25967C6.91737 7.90844 6.57989 7.63721 6.57989 7.63721L5.23871 7.89969Z" fill="#CF701E"/>
<path d="M3.72254 9.8409C3.72254 9.8409 3.50255 9.89215 3.52505 10.0384C3.54755 10.1846 3.79629 10.6459 4.19127 10.5946C4.58625 10.5434 5.20996 10.0896 5.20996 9.9134C5.20996 9.73716 4.89498 9.50342 4.89498 9.50342L3.72254 9.8409Z" fill="#CF701E"/>
<path d="M6.41717 11.6719C6.41717 11.6719 6.33592 12.1444 6.97339 12.3168C8.03584 12.6031 8.14583 11.9581 8.14583 11.9581L6.41717 11.6719Z" fill="#CF701E"/>
<path d="M10.5908 11.8906C10.5908 11.8906 10.3058 12.1756 10.6295 12.3993C10.9532 12.6231 11.322 12.6993 11.6695 12.6606C12.0169 12.6218 12.2544 12.3444 12.2544 12.3444L10.5908 11.8906Z" fill="#CF701E"/>
<path d="M13.2507 8.72583C13.2507 8.72583 12.7133 8.99707 13.1295 9.46579C13.4457 9.82327 14.1694 9.75078 14.4657 9.54954C14.6594 9.41829 14.7169 9.18581 14.6444 9.06831C14.5707 8.95082 13.2507 8.72583 13.2507 8.72583Z" fill="#CF701E"/>
<path d="M11.1708 5.62103C11.1708 5.62103 11.142 6.23474 11.9695 6.21475C13.1269 6.186 12.9807 5.46729 12.9807 5.46729L11.1708 5.62103Z" fill="#CF701E"/>
<path d="M11.7058 4.85228C11.7058 4.85228 11.1633 5.18976 11.1046 5.32851C11.0459 5.46725 10.9209 5.90722 11.5508 5.87848C12.1808 5.84973 12.8108 5.82723 13.0232 5.57099C13.2357 5.31476 13.1845 5.07977 13.097 4.88978C13.0095 4.69979 12.517 3.98108 12.1258 3.98983C11.8696 3.99483 11.7058 4.85228 11.7058 4.85228Z" fill="#593329"/>
<path d="M12.1233 3.98744C12.1233 3.98744 11.7795 3.94369 11.4783 4.47866C11.1783 5.01363 11.1008 5.33237 11.1008 5.33237C11.1008 5.33237 11.712 5.33487 12.0783 5.29112C12.4445 5.24737 12.6932 5.01988 12.6932 5.01988C12.6932 5.01988 12.3945 4.05368 12.1233 3.98744Z" fill="#925849"/>
<path d="M7.96335 6.48593C7.96335 6.48593 7.66336 6.46593 7.64087 6.58217C7.61837 6.69967 7.68461 6.9984 7.78711 7.12965C7.8896 7.26089 8.19709 7.55462 8.50457 7.49588C8.81205 7.43713 8.99454 7.1359 9.04579 7.03215C9.09204 6.93466 9.14204 6.82966 9.05454 6.69842C8.9658 6.56593 7.96335 6.48593 7.96335 6.48593Z" fill="#593329"/>
<path d="M8.40971 6.01723C8.15473 5.98973 7.53976 6.47845 7.64975 6.64595C7.77225 6.82969 7.7935 6.71344 8.13098 6.88218C8.40971 7.02093 8.53096 7.14967 8.70595 7.12717C8.88094 7.10467 9.08968 6.81594 9.05343 6.69844C9.01718 6.58095 8.7547 6.05348 8.40971 6.01723Z" fill="#925849"/>
<path d="M5.52366 4.03867L4.9237 4.45364C4.9237 4.45364 4.9012 4.73488 5.02619 4.88862C5.15118 5.04236 5.18743 5.13736 5.77365 5.13736C6.35987 5.13736 6.90859 5.05736 7.01109 4.63988C7.11358 4.22241 6.93359 3.95492 6.8236 3.80118C6.7136 3.64744 5.52366 4.03867 5.52366 4.03867Z" fill="#593329"/>
<path d="M5.53104 3.38745C5.39604 3.44494 5.07731 3.87867 5.00357 4.06866C4.92982 4.25865 4.91357 4.46114 4.92357 4.46364C5.04481 4.49364 5.75853 4.55989 6.07351 4.50864C6.38849 4.45739 6.64473 4.31865 6.74722 4.13491C6.82847 3.98867 6.82222 3.79493 6.82222 3.79493C6.82222 3.79493 6.60848 3.40995 6.31475 3.29245C6.02226 3.17496 5.58228 3.36495 5.53104 3.38745Z" fill="#925949"/>
<path d="M5.02853 7.92721C5.02853 7.92721 4.97853 8.00346 5.07353 8.1497C5.16852 8.29594 5.35476 8.47093 5.6335 8.55218C5.91223 8.63218 6.10972 8.74217 6.29221 8.72092C6.47595 8.69842 6.83468 8.55218 6.84843 8.04721C6.86343 7.54223 6.76094 7.30725 6.54095 7.08726C6.32096 6.86727 6.16222 6.90477 6.16222 6.90477L5.02853 7.92721Z" fill="#593329"/>
<path d="M5.09246 7.45956C5.01496 7.58955 4.95746 8.01203 5.05246 8.10702C5.14745 8.20202 5.71867 8.20827 5.86866 8.19202C6.01991 8.17577 6.51363 7.94328 6.52113 7.76704C6.52863 7.5908 6.43363 7.35706 6.43363 7.26957C6.43363 7.18207 6.54363 6.97708 6.24364 6.90334C5.94366 6.82959 5.25245 7.18832 5.09246 7.45956Z" fill="#925849"/>
<path d="M4.1024 9.357C4.1024 9.357 3.63117 9.47324 3.60118 9.63449C3.57243 9.79573 3.63992 9.95822 3.80116 10.112C3.96241 10.2657 4.0799 10.4269 4.20364 10.4119C4.32864 10.3969 4.65737 10.1045 4.75986 10.0095C4.86236 9.91447 5.0086 9.74573 4.99485 9.60699C4.9811 9.46824 4.92111 9.32825 4.78986 9.21826C4.65987 9.10826 4.1024 9.357 4.1024 9.357Z" fill="#593329"/>
<path d="M4.2199 8.9396C3.99991 8.9446 3.83117 9.21083 3.75867 9.30583C3.68492 9.40082 3.56118 9.62831 3.61243 9.66456C3.66368 9.70081 3.87616 9.72331 3.97866 9.78955C4.08115 9.8558 4.2349 9.95829 4.35239 9.8633C4.46988 9.7683 4.57238 9.50457 4.63112 9.41707C4.68987 9.32958 4.79237 9.21958 4.79237 9.21958C4.79237 9.21958 4.57113 8.9321 4.2199 8.9396Z" fill="#925849"/>
<path d="M6.9084 10.7044C6.86465 10.7556 6.24219 11.2606 6.24219 11.2606C6.24219 11.2606 6.26469 11.7006 6.40343 11.8031C6.54217 11.9056 6.97465 12.0518 7.25338 12.1106C7.53212 12.1693 8.02959 12.2356 8.15459 12.1843C8.27958 12.1331 8.41082 11.8618 8.41832 11.5106C8.42582 11.1594 8.30083 10.8969 8.16209 10.7356C8.02334 10.5744 7.9121 10.5144 7.9121 10.5144L6.9084 10.7044Z" fill="#593329"/>
<path d="M7.73588 11.3633C7.81712 11.272 7.91212 10.5133 7.91212 10.5133C7.91212 10.5133 7.3484 10.0808 7.11341 10.0521C6.87842 10.0233 6.60094 10.0446 6.51969 10.1321C6.43969 10.2196 6.12471 10.5346 6.15346 10.7395C6.18221 10.9445 6.24095 11.2595 6.24095 11.2595C6.24095 11.2595 7.56713 11.5545 7.73588 11.3633Z" fill="#925849"/>
<path d="M10.4684 11.7244C10.4684 11.7244 10.3971 12.1994 10.9196 12.2894C11.4421 12.3781 11.8758 12.4969 12.102 12.4494C12.3283 12.4019 12.5532 12.0994 12.577 11.8794C12.6007 11.6594 12.6132 11.3419 12.577 11.2557C12.482 11.027 12.042 10.4007 11.8283 10.3345C11.7083 10.2982 11.5946 10.3182 11.5946 10.3182L10.4684 11.7244Z" fill="#593329"/>
<path d="M10.807 10.892C10.5732 11.1507 10.4532 11.5245 10.4657 11.722C10.4782 11.9082 11.3644 11.827 11.4694 11.7632C11.5744 11.6995 11.8032 11.4145 11.8107 11.3195C11.8182 11.2245 11.7457 10.927 11.7332 10.7433C11.7207 10.5595 11.7157 10.3095 11.6144 10.3158C11.5132 10.322 11.1394 10.5245 10.807 10.892Z" fill="#925849"/>
<path d="M13.8043 7.66724C13.8043 7.66724 14.0105 7.69723 14.2643 7.84973C14.5605 8.02722 14.8705 8.47969 14.8467 8.84842C14.823 9.21715 14.5017 9.3834 14.1105 9.38965C13.7193 9.3959 13.3368 9.37465 13.2656 9.17216C13.1943 8.96967 13.8043 7.66724 13.8043 7.66724Z" fill="#593329"/>
<path d="M13.4919 7.79085C13.4306 7.87584 13.2719 8.13583 13.2181 8.46831C13.1644 8.80079 13.2006 9.18077 13.3194 9.24702C13.4381 9.31202 14.0918 8.7483 14.1268 8.6883C14.1631 8.6283 13.9181 7.7196 13.8406 7.67835C13.7631 7.63711 13.5694 7.6846 13.4919 7.79085Z" fill="#925849"/>
<defs>
<radialGradient id="paint0_radial_718_89" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.02019 8.19924) scale(6.6499 6.6499)">
<stop offset="0.507" stop-color="#F29F58"/>
<stop offset="0.7151" stop-color="#F09D56"/>
<stop offset="0.8262" stop-color="#EB9550"/>
<stop offset="0.9141" stop-color="#E18745"/>
<stop offset="0.9898" stop-color="#D47436"/>
<stop offset="1" stop-color="#D27133"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,14 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.8981 13.9368H10.3171V14.803H15.8981V13.9368Z" fill="#AF0C1A"/>
<path d="M9.64575 14.5493V15.213L16.4791 15.2205C16.4791 15.2205 16.4941 14.5781 16.4716 14.5781C16.4504 14.5793 9.64575 14.5493 9.64575 14.5493Z" fill="#DC0D2A"/>
<path d="M9.64575 13.3882L9.66075 14.0594C9.66075 14.0594 16.4579 14.0956 16.4654 14.0744C16.4729 14.0531 16.4804 13.4394 16.4504 13.4244C16.4216 13.4094 9.64575 13.3882 9.64575 13.3882Z" fill="#DC0D2A"/>
<path d="M2.10986 6.98474C2.10986 6.98474 2.10986 9.24213 2.10986 9.31837C2.10986 9.39462 2.16236 9.46211 2.26111 9.46961C2.35985 9.47711 3.65853 9.45461 3.73353 9.46211C3.80852 9.46961 3.89227 9.40212 3.89227 9.26587C3.89227 9.12963 3.89227 6.99349 3.89227 6.99349C3.89227 6.99349 3.43979 6.65351 3.38605 6.66101C3.3323 6.66851 2.10986 6.98474 2.10986 6.98474Z" fill="#858585"/>
<path d="M2.66233 7.52465C2.55734 7.53715 2.51109 7.59964 2.51109 7.72089C2.51109 7.84213 2.49609 8.65709 2.49609 8.73958C2.49609 8.82208 2.50359 8.90582 2.62484 8.91332C2.74608 8.92082 3.2898 8.92832 3.3573 8.92082C3.42479 8.91332 3.47854 8.83833 3.48604 8.73958C3.49354 8.64084 3.48604 7.75838 3.48604 7.68964C3.48604 7.56839 3.40355 7.5309 3.2973 7.5309C3.19106 7.5309 2.78233 7.50965 2.66233 7.52465Z" fill="#B0B0B0"/>
<path d="M13.7444 7.6571L13.7594 6.87964L11.932 6.88714L11.917 7.6196L12.3695 8.11082H12.6382C12.6282 8.83704 12.5957 11.1157 12.5957 11.1157C12.5957 11.1157 12.4932 11.2619 12.377 11.3719C12.2257 11.5156 11.977 11.7044 11.9995 12.1719C12.0207 12.6018 12.4445 12.9268 12.8607 12.9268C13.3057 12.9268 13.6082 12.6018 13.6232 12.2931C13.6382 11.9831 13.5169 11.9706 13.4494 11.9606C13.3432 11.9456 13.2607 12.0206 13.2382 12.1569C13.2157 12.2931 13.0794 12.4668 12.8457 12.4131C12.612 12.3594 12.4607 12.1031 12.6495 11.8544C12.7844 11.6756 13.0419 11.6206 13.0644 11.4694C13.0844 11.3394 13.0694 8.79954 13.0657 8.10957H13.3444L13.7444 7.6571Z" fill="#858587"/>
<path d="M11.4569 13.2582C11.3131 13.3245 11.3331 13.4382 11.3344 13.5545C11.3394 14.0619 11.3631 15.2144 11.3631 15.2144C11.3631 15.2144 11.3969 15.3806 11.6156 15.3806C11.8769 15.3806 11.8969 15.2069 11.8969 15.2069V13.482L12.8831 12.6645L14.1917 13.6257C14.1917 13.6257 14.228 15.1194 14.228 15.2056C14.228 15.2919 14.3142 15.4081 14.5167 15.4006C14.748 15.3919 14.7767 15.2056 14.7767 15.2056C14.7767 15.2056 14.768 13.6057 14.7605 13.4732C14.748 13.2757 14.4517 13.2795 14.4517 13.2795C14.4517 13.2795 13.008 12.1308 12.8781 12.1245C12.7343 12.1183 11.5569 13.212 11.4569 13.2582Z" fill="#FF510F"/>
<path d="M12.3945 9.602C12.3008 9.617 12.2795 9.68199 12.2795 9.76824C12.2795 9.85448 12.2808 10.7119 12.2808 10.7694C12.2808 10.8269 12.2808 10.8844 12.3608 10.8919C12.4408 10.8994 13.2632 10.8994 13.3057 10.8994C13.3707 10.8994 13.407 10.8194 13.407 10.7194C13.407 10.6182 13.412 9.77449 13.407 9.71699C13.3995 9.62325 13.3132 9.602 13.2407 9.5945C13.1682 9.58575 12.4595 9.592 12.3945 9.602Z" fill="#F1901B"/>
<path d="M1.77368 4.73616L6.02971 2.70752C6.02971 2.70752 7.45838 3.025 10.242 3.70872C13.4831 4.50492 14.1368 4.79491 14.1368 4.79491L13.1606 5.06365L12.3669 4.77116L6.04221 3.25749L2.64739 4.83241L1.90242 4.9424L1.77368 4.73616Z" fill="#858585"/>
<path d="M16.5492 6.64477C16.4817 6.57352 14.5593 4.68862 14.4668 4.68862C14.3743 4.68862 7.51213 4.68862 7.51213 4.68862C7.51213 4.68862 6.39844 3.01871 6.25595 2.81247C6.09596 2.57998 5.95222 2.56749 5.80722 2.80622C5.74848 2.90372 4.67728 4.67612 4.67728 4.67612C4.67728 4.67612 1.85243 4.71237 1.68994 4.69987C1.52745 4.68862 1.4812 4.76987 1.4812 4.83862C1.4812 4.90861 1.4812 7.1185 1.5162 7.141C1.5512 7.16349 4.38605 7.15224 4.38605 7.15224L4.3623 15.5768C4.3623 15.5768 4.10981 15.5818 3.93607 15.5818C3.76233 15.5818 3.66609 15.5843 3.66734 15.7168C3.66734 15.7868 3.66484 16.0868 3.66734 16.1918C3.67234 16.3918 3.65109 16.4943 3.78358 16.493C3.89982 16.4918 7.84587 16.493 8.06585 16.493C8.28584 16.493 8.28584 16.3543 8.28584 16.2843C8.28584 16.2143 8.28584 15.8443 8.28584 15.7293C8.28584 15.6143 8.1821 15.5668 8.10085 15.5668C8.01961 15.5668 7.60338 15.5668 7.60338 15.5668V9.16714C7.60338 9.16714 8.0321 9.19089 8.49458 9.12089C8.95706 9.05089 9.10705 8.53842 9.18829 8.16844C9.26954 7.79846 9.24704 7.1235 9.24704 7.1235C9.24704 7.1235 16.4092 7.14224 16.5604 7.08475C16.7104 7.026 16.7554 6.86601 16.5492 6.64477ZM2.16616 5.27609H3.00362L2.16616 6.21104C2.16616 5.92481 2.16616 5.26859 2.16616 5.27609ZM2.62364 6.34229L3.26861 5.61108L4.04107 6.34229H2.62364ZM4.42355 6.13605L3.55484 5.27609H4.42355V6.13605ZM6.87842 13.0944L5.99846 13.9844L5.11351 13.0944H6.87842ZM5.33225 12.6782L6.02471 11.9632L6.73592 12.6695L5.33225 12.6782ZM5.97221 8.59342L4.97477 7.65472H6.89342L5.97221 8.59342ZM7.10215 8.10844V9.65836L6.30595 8.9084L7.10215 8.10844ZM6.79092 10.0646H5.15726L5.96596 9.25088L6.79092 10.0646ZM7.10215 10.7933V12.3657L6.33095 11.6145L7.10215 10.7933ZM6.74842 10.4733L5.98721 11.3083L5.17601 10.4883L6.74842 10.4733ZM7.1659 6.60477H5.40224L7.16715 5.58233V6.60477H7.1659ZM6.01346 3.46369C6.01346 3.46369 6.80842 4.61738 6.78842 4.65613C6.77842 4.67612 6.37344 4.66113 5.98846 4.65863C5.62598 4.65738 5.281 4.66988 5.27725 4.64613C5.26725 4.59863 6.01346 3.46369 6.01346 3.46369ZM4.96602 5.26734H6.75842L4.95852 6.35354L4.96602 5.26734ZM4.88977 8.18844L5.64098 8.9309L4.88977 9.69586V8.18844ZM4.88977 10.842L5.66223 11.6208L4.89477 12.4307L4.88977 10.842ZM4.88977 13.5457L5.66598 14.3219L4.88977 15.1068V13.5457ZM5.11351 15.5856L6.00346 14.6593L6.91216 15.5681C6.50594 15.5668 5.54974 15.5793 5.11351 15.5856ZM7.10215 15.0956L6.3297 14.3181L7.10215 13.5144V15.0956ZM7.63213 6.60477L7.63963 5.49358L8.69707 6.60727C8.05085 6.60477 7.63213 6.60477 7.63213 6.60477ZM9.25829 6.51478L8.00586 5.2361L10.527 5.2286L9.25829 6.51478ZM9.82701 6.60977L11.0482 5.41484L12.3231 6.61602C11.4832 6.61352 10.5982 6.61102 9.82701 6.60977ZM11.4807 5.2261L14.1893 5.2186L12.8519 6.52728L11.4807 5.2261ZM15.598 6.62352C15.5855 6.62352 14.6768 6.62102 13.4781 6.61852C13.7706 6.32354 14.368 5.71982 14.6143 5.47108C14.9455 5.80732 15.5005 6.37229 15.588 6.45978C15.7142 6.58477 15.6367 6.62352 15.598 6.62352Z" fill="#F3911C"/>
<path d="M8.13965 7.33466L8.14465 8.23837L8.93961 8.83084C8.93961 8.83084 9.1296 8.6096 9.21584 8.11712C9.28834 7.70589 9.24834 7.32341 9.24834 7.32341C9.24834 7.32341 8.15215 7.31966 8.13965 7.33466Z" fill="#A7D0D6"/>
<path d="M7.59208 7.09356C7.59208 7.09356 7.60208 7.92976 7.58208 7.93976C7.56208 7.94976 4.366 7.93226 4.30601 7.91226C4.24601 7.89226 4.24726 7.10356 4.27601 7.09356C4.30476 7.08356 7.59208 7.09356 7.59208 7.09356Z" fill="#D15116"/>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,15 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.87954 1.94633C5.58971 1.87134 1.96115 4.59994 2.03115 9.01971C2.0924 12.9058 5.01224 15.8468 8.79579 15.9081C12.5793 15.9693 15.9317 13.4682 15.9517 8.99971C15.9729 4.4962 12.4981 2.02883 8.87954 1.94633Z" fill="#FF2A23"/>
<path d="M8.8895 3.52361C6.27964 3.61236 3.64603 5.55975 3.58478 8.99332C3.52354 12.4269 6.52463 14.3393 8.8895 14.3393C11.8918 14.3393 14.4005 11.9744 14.338 8.82833C14.2767 5.6835 11.9118 3.42112 8.8895 3.52361Z" fill="#FFFDFE"/>
<path d="M8.95078 4.86112C6.5859 4.80738 4.8385 6.89727 4.81725 8.89091C4.79725 10.9271 6.07093 13.0032 8.90953 13.1057C11.7481 13.2082 13.0831 10.9471 13.1243 9.03465C13.1668 7.12225 11.6444 4.92237 8.95078 4.86112Z" fill="#FF2A23"/>
<path d="M6.24718 8.9658C6.22718 10.7545 7.50086 11.7832 9.04328 11.7832C10.4445 11.7832 11.8194 10.8995 11.8194 9.0283C11.8194 7.34214 10.5445 6.33469 9.12578 6.27345C7.7071 6.21095 6.26718 7.23964 6.24718 8.9658Z" fill="white"/>
<path d="M7.51208 8.95208C7.43583 10.0183 8.30328 10.5283 9.08449 10.5283C9.82445 10.5283 10.5544 10.042 10.5757 9.05458C10.5944 8.12713 9.86695 7.56091 9.08449 7.53341C8.27704 7.50591 7.57332 8.08588 7.51208 8.95208Z" fill="#FB2B22"/>
<path d="M9.34201 9.03589L8.92328 9.26838L9.72199 10.4046C9.72199 10.4046 10.3395 10.4521 10.4819 10.2908C10.6244 10.1296 10.4632 9.65461 10.4632 9.65461L9.34201 9.03589Z" fill="#CC1935"/>
<path d="M11.9268 7.80095C11.8143 7.56846 11.6993 7.38222 11.3944 7.43972C11.0906 7.49722 10.5294 7.8197 10.4244 7.97219C10.3282 8.11219 10.2344 8.28593 10.2344 8.28593C10.2344 8.28593 9.15947 8.8734 8.98948 8.96964C8.72325 9.12213 8.90699 9.43212 9.09448 9.39712C9.35071 9.34962 10.4957 8.8834 10.4957 8.8834C10.4957 8.8834 10.7381 8.94089 10.8806 8.94089C11.0231 8.94089 11.8356 8.53966 11.9456 8.46592C12.1268 8.34217 12.0406 8.03844 11.9268 7.80095Z" fill="#ACB1B5"/>
<path d="M10.4594 9.6521C10.4594 9.6521 10.3994 9.86459 10.1807 10.0846C9.95322 10.3121 9.71948 10.4021 9.71948 10.4021L10.4407 11.4595C10.4407 11.4595 10.9769 11.4983 11.3669 11.1183C11.7569 10.7383 11.5869 10.2408 11.5869 10.2408L10.4594 9.6521Z" fill="#C8C8C8"/>
<path d="M11.1744 12.5706L10.4395 11.4582C10.4395 11.4582 10.5619 11.3932 10.7232 11.2832C11.0194 11.0807 11.1844 10.9195 11.3957 10.5895C11.5106 10.412 11.5844 10.2395 11.5844 10.2395L12.7206 10.8007C12.7206 10.8007 12.6956 11.6782 12.3244 12.1057C11.9531 12.5331 11.1744 12.5706 11.1744 12.5706Z" fill="#CC1935"/>
<path d="M11.1744 12.5707L11.8056 13.5169C11.8056 13.5169 12.6293 13.5982 13.3043 12.7895C13.9792 11.9807 13.7992 11.3158 13.7992 11.3158L12.7205 10.7996C12.7205 10.7996 12.5106 11.3045 12.0931 11.7895C11.6756 12.2745 11.1744 12.5707 11.1744 12.5707Z" fill="#C8C8C8"/>
<path d="M12.7405 14.9157L11.8068 13.5182C11.8068 13.5182 12.5105 13.107 13.0105 12.5158C13.5292 11.9021 13.8005 11.3184 13.8005 11.3184L15.3116 12.0696C15.3116 12.0696 14.9717 12.9483 14.2179 13.742C13.4805 14.5157 12.7405 14.9157 12.7405 14.9157Z" fill="#CC1935"/>
<path d="M11.2006 8.17093C11.3419 8.43466 11.5756 8.43716 11.8231 8.33217C12.0706 8.22717 13.2393 7.40097 13.2393 7.40097C13.2393 7.40097 14.8267 8.19968 15.8529 7.7997C16.8791 7.40097 16.8503 6.31728 16.8503 6.31728L16.2429 5.9323C16.2429 5.9323 16.6854 5.70231 16.8278 5.61731C16.8978 5.57481 17.0016 5.41732 16.9441 5.27608C16.8991 5.16609 16.7266 5.12859 16.5841 5.18609C16.4416 5.24358 16.0304 5.37233 16.0304 5.37233C16.0304 5.37233 16.1941 4.69111 16.1654 4.65361C16.1366 4.61612 15.8416 4.46362 15.8416 4.46362C15.8416 4.46362 14.8817 4.46362 14.758 4.51112C14.6342 4.55862 13.6843 5.49982 13.6843 5.49982L13.228 6.43102L13.033 6.846C13.033 6.846 11.7019 7.44222 11.5019 7.52721C11.3031 7.61346 11.0269 7.84969 11.2006 8.17093Z" fill="#006CA9"/>
<path d="M13.3168 6.73108C13.3168 6.73108 13.8768 6.3261 14.2755 6.12611C14.6742 5.92612 15.4142 5.68239 15.6329 5.65614C15.8517 5.62989 15.9417 5.68864 15.9804 5.79113C16.0192 5.89363 15.8904 6.02237 15.7804 6.08112C15.6717 6.13861 15.0917 6.40235 14.4492 6.74358C13.8068 7.08481 13.5018 7.3348 13.2418 7.39355C13.1106 7.42355 12.9418 7.30605 12.9318 7.10981C12.9243 6.95732 12.9543 6.82983 13.0543 6.53734C13.1768 6.17736 13.4455 5.63614 13.6718 5.28366C14.0068 4.76244 14.4892 4.40871 15.023 4.31246C15.5567 4.21622 15.9942 4.35121 16.1042 4.4412C16.2129 4.5312 16.2066 4.62369 16.1679 4.65494C16.1292 4.68619 16.0717 4.71119 15.7817 4.60869C15.5804 4.53745 14.9392 4.53745 14.4892 4.87243C14.0343 5.20991 13.923 5.41865 13.7243 5.78613C13.5255 6.15361 13.3168 6.73108 13.3168 6.73108Z" fill="#1F87FD"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,21 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.76871 13.4269C1.76871 12.2208 5.00604 11.5608 8.99958 11.5608C12.9931 11.5608 16.2304 12.2208 16.2304 13.4269C16.2304 15.1244 12.9931 16.4993 8.99958 16.4993C5.00604 16.4993 1.76871 15.1244 1.76871 13.4269Z" fill="#94D1E0"/>
<path d="M2.39368 7.33472V13.0282H2.39743C2.48117 14.5093 5.38727 15.6993 8.96208 15.6993C12.5369 15.6993 15.443 14.5093 15.5267 13.0282H15.5305V7.33472H2.39368Z" fill="#E63F33"/>
<path d="M3.92859 10.4544C5.28977 11.0219 7.07718 11.3344 8.96208 11.3344C10.847 11.3344 12.6394 11.0331 13.9956 10.4544C15.2142 9.93445 15.338 9.58947 15.5305 9.39073V8.63452C15.1743 9.107 14.573 9.53072 13.7518 9.87196C12.4656 10.4082 10.7645 10.7032 8.96083 10.7032C7.15717 10.7032 5.45601 10.4082 4.16983 9.87196C3.34987 9.52947 2.74866 9.107 2.39117 8.63452V9.55572L3.02239 10.0007C3.29488 10.1644 3.59611 10.3157 3.92859 10.4544Z" fill="#BDBDBD"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.39345 10.3438C6.53472 10.3438 6.64924 10.4583 6.64924 10.5995V15.6605C6.64924 15.8018 6.53472 15.9163 6.39345 15.9163C6.25218 15.9163 6.13766 15.8018 6.13766 15.6605V10.5995C6.13766 10.4583 6.25218 10.3438 6.39345 10.3438Z" fill="#94D1E0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5307 10.3438C11.672 10.3438 11.7865 10.4583 11.7865 10.5995V15.6605C11.7865 15.8018 11.672 15.9163 11.5307 15.9163C11.3894 15.9163 11.2749 15.8018 11.2749 15.6605V10.5995C11.2749 10.4583 11.3894 10.3438 11.5307 10.3438Z" fill="#94D1E0"/>
<path d="M6.25098 14.2556C6.13474 14.2131 6.03099 14.1244 5.971 14.0169C5.86475 13.8256 5.8735 13.5806 5.86975 13.3356C5.86225 12.8019 6.00974 12.6694 6.18348 12.6694H6.60221C6.91595 12.7982 6.91595 13.0569 6.91595 13.2307C6.91595 13.4119 6.91595 13.3469 6.91595 13.5281C6.91595 13.7906 6.92969 14.0531 6.67846 14.2194C6.65471 14.2356 6.45972 14.3318 6.25098 14.2556Z" fill="#94D1E0"/>
<path d="M11.6694 14.2556C11.7819 14.2044 11.8807 14.1181 11.9494 14.0169C12.0981 13.7981 12.0469 13.5806 12.0507 13.3356C12.0582 12.8307 11.9107 12.6694 11.7369 12.6694H11.3182C11.0045 12.7982 11.0045 13.0569 11.0045 13.2307C11.0045 13.4119 11.0045 13.3469 11.0045 13.5281C11.0045 13.7906 11.0095 14.0269 11.2419 14.2194C11.3494 14.3081 11.5557 14.3081 11.6694 14.2556Z" fill="#94D1E0"/>
<path d="M16.0405 11.3856C16.028 10.7969 15.9205 10.6957 15.6405 10.7194C15.6405 9.63947 15.6405 8.66577 15.6405 8.66577L15.228 8.92076L15.258 13.8255C15.258 13.8255 15.6392 13.943 15.6392 13.5318C15.6392 13.4105 15.6392 12.938 15.6392 12.3318C16.0467 12.1893 16.068 11.7556 16.0405 11.3856Z" fill="#94D1E0"/>
<path d="M2.68615 8.85704L2.27367 8.60205C2.27367 8.60205 2.27367 9.6145 2.27367 10.7194C1.99369 10.6957 1.88619 10.7969 1.87369 11.3857C1.87369 11.6306 1.83244 11.8481 1.96119 12.0669C2.03493 12.1894 2.14118 12.2931 2.27367 12.3319C2.27367 12.9143 2.27367 13.3655 2.27367 13.483C2.27367 13.8943 2.6549 13.7768 2.6549 13.7768L2.68615 8.85704Z" fill="#94D1E0"/>
<path d="M15.8942 7.56211C15.6905 5.15599 11.3494 4.47727 8.96207 4.48477C6.59595 4.46477 2.05119 5.19598 2.03244 7.6671C0.728759 9.99448 4.86104 11.5519 8.99957 11.5619C13.1519 11.5719 17.4404 9.91699 15.8942 7.56211Z" fill="#94D1E0"/>
<path d="M8.96208 10.3231C12.5897 10.3231 15.5305 9.09754 15.5305 7.58574C15.5305 6.07394 12.5897 4.84839 8.96208 4.84839C5.33445 4.84839 2.39368 6.07394 2.39368 7.58574C2.39368 9.09754 5.33445 10.3231 8.96208 10.3231Z" fill="#FFCC80"/>
<path d="M8.96205 10.3657C12.3108 10.3657 15.0255 9.23473 15.0255 7.83959C15.0255 6.44446 12.3108 5.31348 8.96205 5.31348C5.61331 5.31348 2.89862 6.44446 2.89862 7.83959C2.89862 9.23473 5.61331 10.3657 8.96205 10.3657Z" fill="#FEF6E0"/>
<path d="M7.73839 9.38452L3.36987 6.48217L4.05984 6.14844L7.94213 9.09828L7.73839 9.38452Z" fill="#FFCC80"/>
<path d="M10.2932 9.38452L14.6605 6.48217L13.9718 6.14844L10.0883 9.09828L10.2932 9.38452Z" fill="#FFCC80"/>
<path d="M8.20462 8.83084C8.02838 8.5921 7.74214 8.53836 7.70589 8.47211L7.5634 8.21712C7.51465 8.12963 7.4784 8.07463 7.43841 8.02213L2.48367 1.64247C2.36617 1.48998 2.14994 1.45498 1.98869 1.56248C1.80995 1.68372 1.77371 1.93246 1.9112 2.0987L6.91343 8.39961C6.97968 8.48211 7.05468 8.55711 7.13842 8.62085L7.34841 8.78209C7.40466 8.82459 7.39466 9.09583 7.5934 9.27832C7.75839 9.42956 8.01588 9.48456 8.17962 9.31207C8.30586 9.18082 8.30836 8.97083 8.20462 8.83084Z" fill="#BA793E"/>
<path d="M9.85326 8.83084C10.0295 8.5921 10.3157 8.53836 10.352 8.47211L10.4945 8.21712C10.5432 8.12963 10.5795 8.07463 10.6195 8.02213L15.5742 1.64247C15.693 1.48998 15.9092 1.45498 16.0692 1.56248C16.2479 1.68372 16.2842 1.93246 16.1467 2.0987L11.1444 8.40086C11.0782 8.48336 11.0032 8.55836 10.9195 8.6221L10.7095 8.78334C10.6532 8.82584 10.6632 9.09708 10.4645 9.27957C10.2995 9.43081 10.042 9.48581 9.87826 9.31332C9.75202 9.18082 9.74952 8.97083 9.85326 8.83084Z" fill="#BA793E"/>
<path d="M3.38108 10.0295C2.65487 9.57824 2.32114 9.12202 2.37989 8.97078C2.41738 8.87453 2.55613 8.87453 2.62237 8.95328C3.27859 9.72574 4.62352 10.2695 5.59472 10.4494C5.69596 10.4682 5.77346 10.6569 5.50097 10.6832C5.26473 10.7044 4.33978 10.6257 3.38108 10.0295Z" fill="white"/>
<path d="M11.4494 14.0019C11.3669 14.0356 11.2744 13.9356 11.2357 13.8044C11.1969 13.6731 11.1744 13.2156 11.1969 13.0819C11.2182 12.9494 11.2457 12.8682 11.3494 12.8682C11.4532 12.8682 11.5369 12.9519 11.5369 13.0557C11.5369 13.0557 11.5244 13.5556 11.5307 13.6769C11.5369 13.8006 11.5319 13.9681 11.4494 14.0019Z" fill="white"/>
<path d="M6.30596 14.0019C6.22347 14.0356 6.13097 13.9356 6.09222 13.8044C6.05348 13.6731 6.03098 13.2156 6.05348 13.0819C6.07597 12.9482 6.10222 12.8682 6.20597 12.8682C6.30971 12.8682 6.39346 12.9519 6.39346 13.0557C6.39346 13.0557 6.38096 13.5556 6.38721 13.6769C6.39346 13.8006 6.38846 13.9681 6.30596 14.0019Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,28 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.68228 8.48206L4.3548 7.77085L3.55359 4.65601L3.34985 4.17104L3.64359 3.44858C3.64359 3.44858 4.05982 3.50732 4.3773 3.86606C4.76353 4.30353 5.3935 5.61221 5.3935 5.61221L5.261 4.57977L5.97971 2.71612C5.97971 2.71612 6.2947 2.68237 6.37844 2.76861C6.52719 2.92111 6.78092 3.99105 6.87092 4.32978C6.96091 4.66851 7.08591 5.13099 7.2434 5.33348C7.40089 5.53597 7.59338 5.58222 7.59338 5.58222L8.1571 3.40483L8.29209 1.42993C8.29209 1.42993 8.48958 1.42868 8.76582 1.48618C8.97456 1.52993 9.11455 1.60117 9.18205 1.68492C9.24954 1.76867 9.25204 3.09985 9.25204 3.09985C9.25204 3.09985 10.4432 2.0849 10.482 2.09615C10.5645 2.11865 10.8207 2.17489 10.9557 2.32239C11.0907 2.46863 10.9782 3.0111 10.877 3.37233C10.7757 3.73356 10.4257 4.90725 10.4257 4.90725L10.742 4.78726L10.812 4.85475L11.4007 3.95105C11.4007 3.95105 11.6132 3.9498 11.7857 4.0348C11.9731 4.12729 12.0856 4.35478 12.0856 4.35478L13.5981 2.16615C13.5981 2.16615 13.7131 2.26864 13.8143 2.36988C13.9155 2.47113 13.9868 2.54238 13.9918 2.64487C13.9968 2.74611 13.6318 3.41983 13.4631 3.75856C13.2943 4.09729 13.0118 4.77476 13.0118 4.77476L14.2418 3.83606C14.2418 3.83606 14.6493 3.89355 14.6618 4.02855C14.673 4.16354 14.4568 4.41353 14.2418 4.78601C14.0268 5.15849 13.6668 5.88095 13.5418 6.12844C13.4168 6.37592 13.5868 6.38842 13.5868 6.38842L15.0605 5.45722C15.0605 5.45722 15.378 5.52222 15.408 5.66971C15.4417 5.83845 15.0767 6.17468 14.8392 6.44467C14.6018 6.71591 14.0943 7.36962 13.8355 7.87834C13.5768 8.38707 13.4031 8.70455 13.4031 8.70455L11.1594 10.9307L5.18976 10.5695L4.68228 8.48206Z" fill="url(#paint0_radial_718_88)"/>
<path d="M12.1458 6.02463C11.9958 6.05963 11.5221 6.87958 11.0796 8.12702C10.8134 8.87698 10.6371 9.69693 10.6471 10.0894C10.6571 10.4819 11.2496 10.3619 11.2496 10.3619L11.5008 9.54694C11.5008 9.54694 11.7821 8.59074 12.1146 7.87703C12.5033 7.03957 13.007 6.5071 12.9695 6.37836C12.9195 6.20587 12.357 5.97463 12.1458 6.02463Z" fill="#FBDE93"/>
<path d="M13.6343 3.89228C13.4243 4.01478 12.9506 4.80724 12.8094 5.10972C12.6681 5.4122 12.6631 5.98092 12.8181 6.05467C13.0093 6.14467 13.2218 5.5022 13.5443 4.96848C13.8668 4.43476 14.248 3.83104 14.248 3.83104C14.248 3.83104 13.8755 3.75104 13.6343 3.89228Z" fill="#FBDE93"/>
<path d="M14.4293 5.4622C14.1081 5.59845 13.5094 6.23716 13.3019 6.5584C12.9194 7.15212 12.6381 7.81583 12.5369 8.08707C12.4369 8.3583 12.3557 8.73078 12.6681 8.55954C12.9806 8.3883 13.3819 7.58334 13.8956 6.85963C14.4905 6.01968 15.063 5.45096 15.063 5.45096C15.063 5.45096 14.6905 5.35096 14.4293 5.4622Z" fill="#FBDE93"/>
<path d="M13.0807 1.87995C12.9569 1.86495 12.6182 2.7649 12.427 3.19738C12.2357 3.62985 11.9807 4.20232 11.9807 4.20232C11.9807 4.20232 11.8333 5.34101 12.0445 5.36101C12.2557 5.38101 12.6695 3.99233 12.9782 3.35612C13.2794 2.73615 13.5969 2.16243 13.5969 2.16243C13.5969 2.16243 13.3219 1.90994 13.0807 1.87995Z" fill="#FBDE93"/>
<path d="M10.8558 4.13481C10.7445 4.25231 10.807 4.85728 10.807 4.85728C10.807 4.85728 11.2395 5.17976 11.3208 5.10851C11.402 5.03727 11.4058 3.94482 11.4058 3.94482C11.4058 3.94482 11.037 3.94357 10.8558 4.13481Z" fill="#FBDE93"/>
<path d="M9.96212 4.95848C9.80587 5.08973 9.91212 5.70469 9.87212 6.25592C9.81212 7.08087 9.57089 8.77078 9.49964 9.26451C9.42839 9.75823 9.3584 10.3007 9.3584 10.3007L10.1334 10.1795C10.1334 10.1795 10.4346 7.87583 10.5558 6.98963C10.6771 6.10342 10.8571 4.84724 10.7371 4.77599C10.6158 4.70725 10.1534 4.79724 9.96212 4.95848Z" fill="#FBDE93"/>
<path d="M9.86827 2.01991C9.67078 2.05741 9.2533 3.09985 9.2533 3.09985C9.2533 3.09985 9.2283 3.85231 9.2283 4.21479C9.2283 4.57727 9.15831 5.00975 9.2783 5.00975C9.39829 5.00975 9.63078 4.31604 9.87202 3.71232C10.1133 3.1086 10.4832 2.09615 10.4832 2.09615C10.4832 2.09615 10.1483 1.96741 9.86827 2.01991Z" fill="#FBDE93"/>
<path d="M7.59727 1.63861C7.46103 1.78235 7.67726 2.15233 7.69726 3.16728C7.71726 4.18347 7.59227 5.5809 7.59227 5.5809C7.59227 5.5809 7.69976 5.5734 7.76726 5.78214C7.82476 5.95838 7.9185 7.02957 8.03849 6.98958C8.15974 6.94958 8.30973 6.23462 8.43097 5.02718C8.55222 3.81974 8.50097 2.88479 8.46097 2.50231C8.42097 2.11983 8.29098 1.42737 8.29098 1.42737C8.29098 1.42737 7.79851 1.42737 7.59727 1.63861Z" fill="#FBDE93"/>
<path d="M5.97981 2.71606C5.97981 2.71606 5.39859 2.75731 5.2386 3.0698C5.07735 3.38228 5.1286 3.53352 5.1586 3.83476C5.1886 4.13599 5.28359 4.73721 5.28359 4.73721C5.28359 4.73721 5.81732 5.40092 6.10855 5.23968C6.39978 5.07969 5.97981 2.71606 5.97981 2.71606Z" fill="#FBDE93"/>
<path d="M3.64363 3.44854C3.64363 3.44854 3.32114 3.38855 3.1299 3.42854C2.93866 3.46854 2.94866 3.64978 3.03991 3.77103C3.1299 3.89227 3.54613 4.64473 3.54613 4.64473C3.54613 4.64473 3.66988 4.64723 3.83112 4.70723C3.99236 4.76722 4.26859 4.91722 4.25734 4.67598C4.23735 4.25475 3.64363 3.44854 3.64363 3.44854Z" fill="#FBDE93"/>
<path d="M4.55231 8.19957C4.55231 8.19957 3.94484 6.97088 3.49236 6.29592C3.03989 5.62095 2.56616 5.09848 2.56616 4.98849C2.56616 4.87724 2.87865 4.76725 3.05864 4.71725C3.23863 4.66726 3.55236 4.65601 3.55236 4.65601C3.55236 4.65601 4.29607 5.91344 4.59855 6.5984C4.90104 7.28337 5.18227 8.08083 5.11228 8.17832C5.04853 8.26582 4.5973 8.20707 4.55231 8.19957Z" fill="#FBDE93"/>
<path d="M4.96121 6.00465C4.91372 6.18089 5.14245 6.98085 5.2837 7.55457C5.42494 8.12829 5.59618 8.732 5.59618 8.732L6.07865 9.7182L6.61238 9.52696C6.61238 9.52696 6.33114 8.98324 6.1699 8.42077C6.00866 7.8558 5.79992 6.10339 5.58493 5.87465C5.43494 5.71341 4.99246 5.89465 4.96121 6.00465Z" fill="#FBDE93"/>
<path d="M6.63232 5.80348C6.50607 5.88598 6.70231 7.22216 6.80356 7.77588C6.9048 8.3296 7.11604 8.91332 7.22604 9.18456C7.33728 9.45579 7.65851 10.1408 7.65851 10.1408C7.65851 10.1408 7.991 10.5332 8.001 10.4932C8.01099 10.4532 8.15224 9.95951 8.15224 9.95951C8.15224 9.95951 7.70851 8.65083 7.63726 8.08711C7.56727 7.52339 7.45602 5.81348 7.34603 5.75349C7.23604 5.69349 6.83356 5.67349 6.63232 5.80348Z" fill="#FBDE93"/>
<path d="M8.87582 5.83344C8.72458 5.97343 8.57459 7.82584 8.51334 8.50955C8.4521 9.19326 8.4021 10.1995 8.4021 10.1995L9.07581 10.1495C9.07581 10.1495 9.32705 7.66459 9.36705 7.34211C9.40705 7.01963 9.58829 6.28592 9.49829 5.99343C9.4083 5.70345 9.00582 5.71345 8.87582 5.83344Z" fill="#FBDE93"/>
<path d="M4.35233 8.42834C4.30983 8.52459 5.62351 15.713 5.64476 15.8205C5.66601 15.9279 5.62351 16.0254 5.82725 16.1217C6.03224 16.2179 6.35222 16.2692 6.55721 16.2917C6.7622 16.3129 8.17338 16.3154 8.18463 16.1442C8.19587 15.9717 12.4144 9.82702 12.4144 9.82702C12.4144 9.82702 13.1919 8.52334 13.1381 8.41584C13.0844 8.30835 12.8744 8.35835 12.6456 8.51459C12.4169 8.67208 12.2057 9.06081 11.5632 9.49204C10.867 9.95951 9.94453 10.0583 9.01708 10.047C8.16213 10.0358 7.14593 9.88077 6.41847 9.34204C5.7785 8.86707 5.58976 8.52459 5.01979 8.37335C4.51732 8.2396 4.35233 8.42834 4.35233 8.42834Z" fill="#FD703F"/>
<path d="M5.34619 8.89825L6.53863 16.2941C6.53863 16.2941 7.9598 16.4991 9.06725 16.5203C10.1759 16.5416 11.4971 16.3104 11.4971 16.3104L12.4571 13.0655L13.272 8.7845C13.272 8.7845 13.0708 8.58326 12.8445 8.7495C12.6708 8.877 12.1346 9.70695 11.1871 10.0732C10.1184 10.4857 8.19229 10.5157 6.98985 10.0082C5.88741 9.54446 5.55118 8.94074 5.34619 8.89825Z" fill="#FF2A23"/>
<path d="M12.1883 16.0829C11.9458 16.2279 11.3809 16.3253 11.3809 16.3253C11.3809 16.3253 11.9408 13.603 12.2521 12.1818C12.5633 10.7606 13.012 8.35701 13.012 8.35701C13.012 8.35701 13.192 8.32827 13.377 8.35076C13.587 8.37701 13.712 8.43826 13.7545 8.50451C13.8232 8.612 13.4095 10.6194 13.0045 12.5793C12.6345 14.3792 12.3171 16.0066 12.1883 16.0829Z" fill="#DC0D25"/>
<defs>
<radialGradient id="paint0_radial_718_88" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.0729 0.0159706) scale(10.3905 10.3905)">
<stop offset="0.447" stop-color="#FCC216"/>
<stop offset="0.5897" stop-color="#FABB16"/>
<stop offset="0.7939" stop-color="#F3A814"/>
<stop offset="0.9212" stop-color="#ED9913"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1,33 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.73857 12.2331C2.73857 12.2331 2.61858 13.0181 2.89732 13.7105C3.1298 14.2905 3.73227 15.0542 4.88471 15.6367C6.03715 16.2191 7.36083 16.4241 9.31448 16.4641C11.2681 16.5054 13.2755 16.0492 14.2392 15.203C15.2042 14.3555 15.5416 13.643 15.5142 12.8693C15.4867 12.0956 15.0754 11.8019 15.0754 11.8019L2.73857 12.2331Z" fill="#FCB745"/>
<path d="M3.2974 11.4443L2.72743 12.0143C2.72743 12.0143 2.57244 12.7768 3.4324 13.6017C4.08361 14.2254 5.23605 15.3091 9.06335 15.3379C12.6457 15.3654 13.7719 14.5779 14.4631 14.0217C15.1555 13.4655 15.4155 12.4993 15.3468 12.228C15.2793 11.9568 15.0668 11.8006 15.0668 11.8006L3.2974 11.4443Z" fill="#E78B20"/>
<path d="M3.54113 12.2581C3.54113 12.2581 3.37989 12.5319 3.43238 12.7056C3.47363 12.8419 3.55488 13.3981 5.16854 14.103C6.02225 14.4768 7.47467 14.7818 9.36082 14.7268C11.5982 14.6618 13.3906 14.2118 14.2581 13.3968C15.1468 12.5631 14.9768 12.0669 14.9768 12.0669L3.54113 12.2581Z" fill="#FFD290"/>
<path d="M3.07979 10.3582C3.07979 10.3582 2.54482 10.9031 2.45607 11.1456C2.37233 11.3744 2.38358 11.7068 2.53107 11.9756C2.72981 12.3355 2.9773 12.373 3.28353 12.5568C3.61476 12.7543 4.13849 13.3705 5.42717 13.6417C6.71585 13.913 8.41201 14.0355 9.32071 14.0355C10.2294 14.0355 11.4369 14.0217 12.563 13.6417C13.6892 13.2618 14.9779 12.5155 15.2766 11.8506C15.5754 11.1856 15.2904 10.7919 15.2904 10.7919L14.6392 10.3582H3.07979Z" fill="#6D544D"/>
<path d="M2.76732 9.66693C2.76732 9.66693 2.35235 9.64818 2.25235 10.2769C2.17986 10.7344 2.50984 11.0231 2.50984 11.0231C2.50984 11.0231 2.33485 11.2743 2.53734 11.5931C2.72733 11.8918 2.93107 11.9593 3.35104 12.0268C3.65353 12.0756 3.81227 11.9856 4.206 12.203C4.69972 12.4755 4.91096 12.963 5.58968 13.1117C6.26839 13.2605 7.02835 13.1255 7.0546 13.0168C7.08085 12.908 6.10465 12.5018 4.34099 11.5106C3.74602 11.1769 3.20105 10.7231 3.05231 10.4931C2.90357 10.2631 2.76732 9.66693 2.76732 9.66693Z" fill="#9B7169"/>
<path d="M11.302 12.9768C11.302 13.1531 11.8857 13.0718 12.157 12.9231C12.4282 12.7743 12.8619 12.3943 13.2556 12.2306C13.6494 12.0681 13.7981 12.2169 14.3681 11.9456C14.9381 11.6744 15.2505 11.0369 15.2505 11.0369C15.2505 11.0369 15.528 10.6944 15.5505 10.4544C15.5855 10.0882 15.5493 9.91072 15.2918 9.82948C15.0343 9.74823 14.4731 10.5919 14.3918 10.6869C14.3106 10.7819 13.1082 11.6606 12.6332 11.9994C12.1582 12.3381 11.302 12.8818 11.302 12.9768Z" fill="#9B7169"/>
<path d="M3.29733 9.20577C3.29733 9.20577 2.61862 9.28702 2.52362 9.572C2.42863 9.85699 2.72736 10.277 3.51357 10.8069C4.15853 11.2407 6.36842 12.2844 7.24462 12.8281C8.24832 13.4518 9.09827 14.323 9.23952 14.3343C9.41576 14.348 10.6394 13.0481 12.3331 11.9056C13.943 10.8182 15.0467 10.5494 15.4404 9.74824C15.6517 9.31826 15.1417 9.06953 15.1417 9.06953L8.60205 6.58716L3.29733 9.20577Z" fill="#FFE869"/>
<path d="M9.32088 13.6018C9.52337 13.5793 10.5821 12.5568 11.8307 11.7706C13.0794 10.9844 14.9243 10.142 15.1956 9.66823C15.4668 9.1945 14.8156 8.88077 14.8156 8.88077L7.21724 7.60583L2.76747 9.35574C2.76747 9.35574 2.52374 9.43699 2.68623 9.64073C2.84872 9.84447 3.10621 10.1832 3.67618 10.5357C4.36864 10.9644 6.14604 11.6756 7.27099 12.2856C8.39843 12.8956 9.07714 13.628 9.32088 13.6018Z" fill="#FFBE01"/>
<path d="M3.37988 9.55827L3.61612 10.1907C3.61612 10.1907 5.31603 11.9044 8.6971 11.9719C12.0619 12.0394 13.9481 10.4382 13.9481 10.4382L14.3781 9.35828L3.37988 9.55827Z" fill="#F4482B"/>
<path d="M7.81222 10.6031L7.54598 11.0294C7.54598 11.0294 8.1147 11.3706 8.27844 11.4294C8.51343 11.5131 8.74467 11.4831 9.0909 11.3756C9.43713 11.2694 10.8746 10.6969 10.8746 10.6969C10.8746 10.6969 12.0632 10.7144 12.8095 10.7281C13.4707 10.7406 14.1331 10.5032 14.4794 10.1032C14.8256 9.7032 15.3631 8.84574 15.3631 8.84574C15.3631 8.84574 15.6431 8.7795 15.8293 8.56576C16.0155 8.35202 16.0293 8.16578 16.0293 8.16578C16.0293 8.16578 15.9755 7.8058 15.7768 7.7133C15.5768 7.61956 6.66978 7.07458 6.66978 7.07458L3.10122 7.34082L2.5 8.57951L4.48365 10.2707L7.27975 10.4307L7.81222 10.6031Z" fill="#A6B732"/>
<path d="M2.68614 7.48837C2.4849 7.39713 2.27366 7.40838 2.17992 7.67461C2.08617 7.94085 2.11492 8.05834 2.11492 8.05834C2.11492 8.05834 1.84118 8.11584 1.80744 8.45957C1.76744 8.87205 2.21991 9.01829 2.21991 9.01829C2.21991 9.01829 2.24116 9.17828 2.4199 9.33828C2.62364 9.52077 2.82738 9.50202 2.82738 9.50202C2.82738 9.50202 3.36485 10.0307 3.72483 10.2432C4.08482 10.4557 4.60354 10.6032 5.12351 10.5895C5.64348 10.5757 5.81597 10.337 6.2022 10.4695C6.58843 10.602 7.61213 11.1407 8.02461 11.1494C8.67707 11.1632 10.0882 10.1507 10.4082 10.1645C10.7282 10.1782 12.1794 10.4045 12.9518 10.4045C13.7243 10.4045 14.0043 10.1782 14.1768 9.95199C14.3493 9.72576 14.643 9.15329 14.5493 9.13954C14.4555 9.12579 13.843 9.63201 13.4043 9.59201C12.9643 9.55201 12.5794 9.29953 12.0331 9.29953C11.4869 9.29953 8.82456 9.64576 8.65082 9.72576C8.47708 9.80575 7.73212 10.3782 7.58588 10.352C7.43964 10.3257 6.93341 9.8995 6.76092 9.80575C6.58843 9.71201 4.77228 9.65201 4.51104 9.69951C4.32355 9.73326 4.05857 9.7795 4.05857 9.7795C4.05857 9.7795 4.08482 9.36702 3.85858 9.12704C3.63234 8.88705 2.88613 8.47457 2.88613 8.47457C2.88613 8.47457 2.97862 7.62087 2.68614 7.48837Z" fill="#C2DD1F"/>
<path d="M14.4956 8.20707C14.4956 8.20707 15.3081 8.20707 15.5343 8.19332C15.7605 8.17957 16.0268 8.16707 16.0268 8.16707C16.0268 8.16707 15.9493 7.79834 15.8318 7.6021C15.5918 7.19962 15.0143 7.08838 15.0143 7.08838L14.4956 8.20707Z" fill="#C2DD1F"/>
<path d="M2.49988 6.99588L2.41113 7.38836C2.41113 7.38836 3.63982 9.56449 9.16203 9.64699C12.3944 9.69574 14.2518 8.71454 15.0717 7.91208C15.5629 7.43085 15.5792 6.96338 15.5792 6.96338L2.49988 6.99588Z" fill="#DE8010"/>
<path d="M2.22002 5.91342C2.15003 7.00086 2.41501 7.39084 2.41501 7.39084C2.41501 7.39084 4.24242 9.04575 8.90967 9.122C13.6932 9.1995 15.2481 7.61333 15.5206 7.15085C15.6943 6.85712 15.8331 6.2809 15.7231 5.44094C15.5406 4.05352 13.9432 1.42115 8.88717 1.44115C3.83119 1.46115 2.32252 4.33475 2.22002 5.91342Z" fill="url(#paint0_radial_721_184)"/>
<path d="M7.0235 2.79484C7.06225 3.00733 6.10355 3.7148 5.96481 3.03608C5.83482 2.39736 6.98725 2.5986 7.0235 2.79484Z" fill="#FDEAC8"/>
<path d="M7.36586 4.12605C7.41586 4.25729 8.39831 4.44603 8.29331 3.89231C8.18332 3.31609 7.26337 3.85606 7.36586 4.12605Z" fill="#FDEAC8"/>
<path d="M8.22709 2.60604C8.25209 2.79478 9.39703 3.01477 9.37328 2.4823C9.34454 1.82483 8.19835 2.3873 8.22709 2.60604Z" fill="#FDEAC8"/>
<path d="M6.33846 4.56352C6.34471 4.60602 6.06348 4.82725 5.77599 4.9135C5.5235 4.98975 5.26477 4.95725 5.23602 4.64351C5.20852 4.34103 5.42351 4.23104 5.681 4.25728C5.97723 4.28603 6.32596 4.48227 6.33846 4.56352Z" fill="#FDEAC8"/>
<path d="M8.71705 5.44724C8.66955 5.2485 7.59336 4.97226 7.66586 5.60098C7.7396 6.2272 8.7683 5.65848 8.71705 5.44724Z" fill="#FDEAC8"/>
<path d="M9.72448 4.016C9.67198 3.95475 9.42574 3.966 9.12576 4.04475C8.90952 4.10224 8.74953 4.34598 8.89952 4.57722C9.07451 4.84721 9.36824 4.73346 9.55573 4.50472C9.70823 4.31973 9.78322 4.08475 9.72448 4.016Z" fill="#FDEAC8"/>
<path d="M10.8771 2.55482C10.8434 2.51858 10.5971 2.49358 10.3959 2.59107C10.1934 2.68857 10.0184 2.92106 10.2059 3.13854C10.3959 3.35728 10.6784 3.23104 10.8121 2.9998C10.9184 2.81356 10.9284 2.60982 10.8771 2.55482Z" fill="#FDEAC8"/>
<path d="M11.6495 4.45727C11.617 4.25353 10.712 3.89105 10.7183 4.44602C10.7245 5.01474 11.6782 4.64101 11.6495 4.45727Z" fill="#FDEAC8"/>
<path d="M12.9005 3.45228C12.8705 3.29604 11.7193 3.16979 11.9156 3.75101C12.1193 4.35723 12.9368 3.64227 12.9005 3.45228Z" fill="#FDEAC8"/>
<path d="M11.3157 5.49714C11.3232 5.32215 10.447 4.92092 10.382 5.44589C10.317 5.97087 11.3057 5.72463 11.3157 5.49714Z" fill="#FDEAC8"/>
<defs>
<radialGradient id="paint0_radial_721_184" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8.9328 2.94018) rotate(89.3583) scale(5.50481 9.59198)">
<stop offset="0.517" stop-color="#DF8016"/>
<stop offset="0.6426" stop-color="#E58C21"/>
<stop offset="1" stop-color="#F5AC3C"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1,27 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.53148 15.0048C4.03297 14.9075 4.32384 14.2326 4.18115 13.4973C4.03847 12.762 3.51626 12.2448 3.01478 12.3421C2.51329 12.4394 2.22242 13.1144 2.3651 13.8497C2.50779 14.585 3.02999 15.1021 3.53148 15.0048Z" fill="#616161"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.80501 13.7743C2.59761 12.0082 2.47024 10.1592 2.55166 8.48633L3.30073 8.52278C3.22216 10.1373 3.34478 11.9405 3.54985 13.6868L2.80501 13.7743Z" fill="#616161"/>
<path d="M15.6327 13.8482C15.7754 13.1129 15.4845 12.4379 14.983 12.3406C14.4815 12.2433 13.9593 12.7605 13.8166 13.4958C13.674 14.2311 13.9648 14.906 14.4663 15.0034C14.9678 15.1007 15.49 14.5835 15.6327 13.8482Z" fill="#616161"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4494 13.6868C14.6545 11.9405 14.7771 10.1373 14.6985 8.52278L15.4476 8.48633C15.529 10.1592 15.4016 12.0082 15.1942 13.7743L14.4494 13.6868Z" fill="#616161"/>
<path d="M15.2405 9.32571C14.5943 9.29572 14.0581 8.81074 13.9743 8.16953C13.8806 7.44331 13.6394 6.60461 13.0344 5.79965C12.0832 4.53597 10.6908 3.86725 9.00586 3.86725C7.82467 3.86725 6.16476 4.20224 4.96357 5.79965C4.3511 6.61461 4.11237 7.47081 4.02112 8.20827C3.94488 8.81949 3.4499 9.29322 2.83368 9.32197L2.75869 9.32571C2.32621 9.34571 1.96873 8.98823 1.99373 8.557C2.05373 7.50331 2.31871 5.63216 3.37491 4.22723C4.69734 2.46733 6.69723 1.49988 9.00586 1.49988C11.3057 1.49988 13.3006 2.46733 14.6231 4.22473C15.6805 5.63091 15.9442 7.50331 16.0055 8.55576C16.0305 8.98823 15.673 9.34571 15.2405 9.32571Z" fill="#616161"/>
<path d="M5.84104 16.4103C4.6261 16.5791 3.23243 15.6829 2.95119 13.6092C2.67121 11.5443 3.65241 10.2894 4.64735 10.0969L5.84104 16.4103Z" fill="#424242"/>
<path d="M5.91105 9.66946L4.8536 9.8507C4.42613 9.93319 4.09114 10.2644 4.00365 10.6919C3.88616 11.2681 3.81116 12.1893 4.05365 13.4455C4.29738 14.7004 4.71111 15.5266 5.03734 16.0179C5.27733 16.3804 5.71231 16.5628 6.13979 16.4791L7.19723 16.2979C7.70845 16.1991 7.8347 14.6342 7.47847 12.8043C7.12348 10.9744 6.42227 9.57071 5.91105 9.66946Z" fill="#757575"/>
<path opacity="0.5" d="M7.30094 16.0162C7.72721 15.9335 7.80678 14.4958 7.47867 12.8049C7.15057 11.1141 6.53903 9.81048 6.11276 9.8932C5.6865 9.97591 5.60693 11.4137 5.93503 13.1045C6.26314 14.7953 6.87468 16.0989 7.30094 16.0162Z" fill="#424242"/>
<path d="M7.23096 12.853C7.45345 13.9979 7.5022 14.9516 7.34095 14.9829C7.17971 15.0141 6.86723 14.1117 6.64599 12.9667C6.4235 11.8218 6.37476 10.8681 6.536 10.8368C6.69724 10.8056 7.00847 11.7081 7.23096 12.853Z" fill="url(#paint0_linear_715_302)"/>
<path d="M12.1582 16.4103C13.3731 16.5791 14.7668 15.6829 15.0481 13.6092C15.328 11.5443 14.3468 10.2894 13.3519 10.0969L12.1582 16.4103Z" fill="#424242"/>
<path d="M12.0882 9.66946L13.1456 9.8507C13.5731 9.93319 13.9081 10.2644 13.9956 10.6919C14.1131 11.2681 14.1881 12.1893 13.9456 13.4455C13.7018 14.7004 13.2881 15.5266 12.9619 16.0179C12.7219 16.3804 12.2869 16.5628 11.8594 16.4791L10.802 16.2979C10.2908 16.1991 10.1645 14.6342 10.5208 12.8043C10.8757 10.9744 11.577 9.57071 12.0882 9.66946Z" fill="#757575"/>
<path opacity="0.5" d="M12.0637 13.1035C12.3918 11.4127 12.3122 9.97494 11.886 9.89222C11.4597 9.8095 10.8482 11.1131 10.5201 12.804C10.1919 14.4948 10.2715 15.9325 10.6978 16.0153C11.1241 16.098 11.7356 14.7943 12.0637 13.1035Z" fill="#424242"/>
<path d="M10.7683 12.853C10.5458 13.9979 10.4971 14.9516 10.6583 14.9829C10.8196 15.0141 11.132 14.1117 11.3533 12.9667C11.5758 11.8218 11.6245 10.8681 11.4633 10.8368C11.302 10.8056 10.9908 11.7081 10.7683 12.853Z" fill="url(#paint1_linear_715_302)"/>
<path d="M15.8855 8.76198C15.5318 9.00197 15.0955 8.73198 14.9618 8.367C14.8068 7.45705 14.5006 6.40086 13.9006 5.52965C12.7769 3.80599 10.8882 2.94229 8.9996 2.94104C7.11095 2.94229 5.2223 3.80599 4.09861 5.52965C3.49864 6.40086 3.19365 7.4583 3.03741 8.367C2.90367 8.73198 2.46744 9.00197 2.11371 8.76198C2.08496 8.74198 2.04121 8.69199 1.99622 8.66449C2.03121 9.04572 2.3637 9.34195 2.75868 9.32445L2.86742 9.31945C3.63988 9.3257 3.94611 8.83448 4.01736 8.24076C4.1061 7.49705 4.34234 6.62585 4.96481 5.79964C6.1635 4.20597 7.81841 3.86849 8.9996 3.86724C10.1808 3.86849 11.8357 4.20597 13.0344 5.79964C13.6469 6.6146 13.8856 7.4708 13.9768 8.20826C14.0543 8.82698 14.3543 9.3257 15.198 9.3232L15.2393 9.3257C15.6342 9.34445 15.9667 9.04697 16.0017 8.66574C15.958 8.69199 15.9142 8.74198 15.8855 8.76198Z" fill="#424242"/>
<path d="M2.83121 6.20091C2.71497 6.11841 2.62372 6.02717 2.78996 5.3997C2.9912 4.64349 3.9249 3.17982 5.80605 2.20987C6.15478 2.02988 7.00224 1.76114 7.26098 2.22737C7.50596 2.66984 6.32228 2.99233 5.9048 3.21607C4.81236 3.80353 4.02365 4.63224 3.59992 5.28721C3.46493 5.49595 3.1087 6.3959 2.83121 6.20091Z" fill="#757575"/>
<defs>
<linearGradient id="paint0_linear_715_302" x1="6.67969" y1="12.9608" x2="7.03923" y2="12.891" gradientUnits="userSpaceOnUse">
<stop offset="0.2016" stop-color="#212121"/>
<stop offset="1" stop-color="#212121" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_715_302" x1="11.3196" y1="12.9609" x2="10.96" y2="12.8911" gradientUnits="userSpaceOnUse">
<stop offset="0.2016" stop-color="#212121"/>
<stop offset="1" stop-color="#212121" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,25 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.9153 14.4092L15.1966 14.0943C15.1916 14.0555 15.1854 14.0168 15.1754 13.9793C15.1216 13.7705 14.9241 13.5168 14.5979 13.443L14.5316 12.5068L13.8317 12.5468L13.9192 13.543C13.6592 13.6855 13.5392 13.9368 13.5479 14.2255L12.813 14.8217L13.2505 15.3704L13.8417 14.873C13.9892 14.9867 14.1829 15.0817 14.4229 15.068C14.7116 15.0517 14.9091 14.9017 15.0316 14.7492L15.6678 15.0905L15.9153 14.4092Z" fill="#878787"/>
<path d="M6.0135 14.1379C5.88226 12.4643 4.50233 11.9118 3.49989 11.9743C2.35495 12.0468 1.29375 12.8843 1.3125 14.3129C1.32875 15.5691 2.11621 16.5553 3.72737 16.5553C5.01731 16.5566 6.1085 15.3554 6.0135 14.1379ZM3.67113 15.7279C2.69993 15.7454 2.13996 15.1791 2.13121 14.3292C2.12371 13.533 2.77492 12.8418 3.59488 12.8655C4.38234 12.888 4.96231 13.3342 5.05855 14.1617C5.1548 14.9891 4.52983 15.7129 3.67113 15.7279Z" fill="#4D453C"/>
<path d="M3.58723 13.1605C2.95101 13.1268 2.39354 13.5655 2.38604 14.3692C2.37854 15.2204 3.14975 15.5141 3.59473 15.5229C4.0397 15.5316 4.74092 15.2054 4.76342 14.4179C4.78841 13.5817 4.33469 13.1993 3.58723 13.1605Z" fill="#858585"/>
<path d="M14.1731 11.6731C12.9419 11.7556 11.867 12.8155 11.882 14.1467C11.8982 15.5466 12.9719 16.5403 14.3319 16.5403C15.6605 16.5403 16.7642 15.5616 16.7417 14.0742C16.718 12.4606 15.4693 11.5856 14.1731 11.6731ZM14.3644 15.5779C13.6094 15.5704 13.0357 14.9729 12.9644 14.3217C12.8694 13.4505 13.4157 12.8168 14.1894 12.7543C14.9931 12.6905 15.6605 13.168 15.748 14.0267C15.8355 14.8854 15.1593 15.5866 14.3644 15.5779Z" fill="#4D453C"/>
<path d="M1.89367 12.2531C1.93992 12.3343 2.45864 12.2931 2.85612 12.4681C3.11486 12.5818 3.39734 12.8818 3.35735 13.2955C3.31735 13.7092 3.2386 14.0905 3.2386 14.0905L3.96981 14.3292C3.96981 14.3292 4.25605 13.6855 4.41479 13.518C4.57353 13.3505 4.93226 13.0168 5.45723 13.088C5.86096 13.143 6.31594 13.5093 6.31594 13.5093C6.31594 13.5093 6.0297 11.7756 5.99721 11.7594C5.96596 11.7431 4.39104 11.8231 4.39104 11.8231C4.39104 11.8231 3.67483 11.5294 2.94362 11.7119C2.21116 11.8956 1.86117 12.1981 1.89367 12.2531Z" fill="#DB0D2A"/>
<path d="M4.2399 11.498L3.48494 13.7892C3.48494 13.7892 3.1962 13.7904 3.08746 14.1867C2.99996 14.5054 3.21495 14.7116 3.41369 14.7666C3.64868 14.8329 3.95241 14.7279 4.02616 14.4566C4.12115 14.1067 3.93116 13.9479 3.93116 13.9154C3.93116 13.8829 4.77362 11.5618 4.77362 11.5618L4.2399 11.498Z" fill="#E1D9DC"/>
<path d="M6.84961 13.3155L8.20829 14.8479L10.8519 14.8354L12.4706 14.3529L12.2493 13.488L10.9394 12.8705C10.9394 12.8705 11.5944 11.0169 11.4581 10.9556C11.3219 10.8944 9.7907 9.86816 9.7907 9.86816L6.88711 10.8444L6.84961 13.3155Z" fill="#2F2F2F"/>
<path d="M12.3868 11.2294C12.3868 11.2294 11.9393 11.6369 11.6793 11.9981C11.4969 12.2531 11.4456 12.5481 11.4456 12.5481L11.8281 12.7206C11.8281 12.7206 12.0593 12.1906 12.6005 11.8756C13.2218 11.5131 13.6942 11.5594 13.768 11.7194C13.8417 11.8794 13.5367 11.9331 13.1005 12.3868C12.7793 12.7206 12.6068 13.0418 12.6068 13.0418C12.6068 13.0418 14.053 13.6343 14.053 13.7093C14.053 13.783 13.793 14.5242 13.793 14.5242L12.1493 14.1292C12.1493 14.1292 11.0869 13.808 10.9756 14.1292C10.8644 14.4505 11.3581 14.6355 11.3581 14.6355L10.9006 14.9317L9.47946 14.3142C9.47946 14.3142 9.39571 13.9218 9.83819 13.0543C10.5069 11.7406 11.5056 11.0769 11.5056 11.0769L12.3868 11.2294Z" fill="#5E6268"/>
<path d="M10.9369 14.7805C10.9406 14.833 10.9806 14.8955 11.1481 14.8867C11.3156 14.878 12.993 14.9042 13.1955 14.8605C13.398 14.8167 16.1091 13.6405 16.1628 13.5343C16.2016 13.4568 16.2078 13.1955 16.0816 12.9268C15.9404 12.6268 15.7991 12.4768 15.6141 12.5118C15.4291 12.5468 11.6518 14.348 11.3693 14.4617C11.0869 14.5767 10.9281 14.6567 10.9369 14.7805Z" fill="#E0E0E0"/>
<path d="M12.9218 14.2767C12.9518 14.348 13.2455 14.338 13.5868 14.1817C13.928 14.0255 16.0991 12.963 16.0991 12.963C16.0991 12.963 16.0629 12.8543 15.9991 12.753C15.9491 12.6743 15.8654 12.5918 15.8654 12.5918C15.8654 12.5918 13.3968 13.8492 13.258 13.918C13.078 14.008 12.8656 14.143 12.9218 14.2767Z" fill="#FEFEFE"/>
<path d="M15.063 9.95188L15.9642 10.7481C15.9642 10.7481 16.1654 10.4256 16.1654 10.3944C16.1654 10.3619 15.5617 9.59814 15.5617 9.59814L15.063 9.95188Z" fill="#484D51"/>
<path d="M12.5669 11.2956C12.5669 11.2956 13.793 10.7694 14.308 10.5394C15.1142 10.1807 15.9392 9.71821 16.0867 9.63072C16.3529 9.47198 16.3504 9.32323 16.2767 9.26199C16.2029 9.20074 16.0054 9.28698 15.9054 9.29948C15.8054 9.31198 15.5105 9.32448 15.5105 9.32448L13.2993 9.92945C13.2993 9.92945 11.8294 10.8557 11.8656 10.8557C11.9019 10.8557 12.5669 11.2956 12.5669 11.2956Z" fill="#DC0D2A"/>
<path d="M9.77075 10.1719C9.77075 10.1719 10.737 11.1119 10.8757 11.1819C11.0157 11.2519 11.2819 11.2831 11.5044 11.2394C11.7269 11.1956 12.9531 10.2669 13.4806 10.1019C14.008 9.93693 14.9167 9.91193 15.2155 9.74569C15.5142 9.57945 15.5104 9.32446 15.5104 9.32446L12.9156 9.50445C12.9156 9.50445 12.3656 10.3882 11.3332 10.3307C10.5395 10.2857 9.94199 9.61945 9.94199 9.61945L9.77075 10.1719Z" fill="#464C4F"/>
<path d="M12.7831 11.9943C13.0119 12.1156 13.1206 11.8518 13.0631 11.7343C12.9419 11.4869 12.4919 10.7831 12.3769 10.7369C12.2819 10.6994 11.767 11.0731 11.7157 11.1119C11.6645 11.1494 11.6832 11.2306 11.792 11.3094C11.9319 11.4106 12.6057 11.9006 12.7831 11.9943Z" fill="#C8C8C8"/>
<path d="M5.00966 11.0169L3.4585 10.7057L4.22596 9.22699L5.04966 8.40329L5.53714 8.22705C5.53714 8.22705 5.12591 8.77327 4.90467 9.117C4.76718 9.33074 4.62218 9.68197 4.62218 9.68197C4.62218 9.68197 4.79343 9.64447 4.92967 9.62822C5.06591 9.61197 5.21715 9.60072 5.21715 9.60072C5.21715 9.60072 5.17216 9.30824 5.2359 9.157C5.29965 9.0045 5.43214 8.90576 5.52839 8.89701C5.62463 8.88951 5.68838 8.93701 5.68838 8.93701C5.68838 8.93701 5.67463 8.70577 5.69588 8.64077C5.73588 8.52078 6.07211 8.37954 6.48834 8.22705C6.90706 8.0733 7.37704 7.85956 7.50328 7.88956C7.58203 7.90831 7.76702 8.26579 7.67077 8.36954C7.57453 8.47328 6.08836 9.1695 6.08836 9.1695L6.33584 9.84946L5.00966 11.0169Z" fill="#464C4F"/>
<path d="M7.70974 10.7943L9.73088 10.7206L10.1246 11.1318L8.80218 11.953L7.521 11.6405L7.40601 10.8443L7.70974 10.7943Z" fill="#464C4F"/>
<path d="M6.84709 12.0594L5.78339 13.2205C5.78339 13.2205 6.17337 13.913 6.28087 14.2105C6.39336 14.5205 6.5521 15.0805 6.5521 15.0805C6.5521 15.0805 10.5844 15.1954 10.7731 15.1704C10.9619 15.1454 11.1756 14.9242 11.0769 14.8342C10.9781 14.7442 9.44195 14.1118 9.44195 14.1118C9.44195 14.1118 8.23451 14.1942 8.03702 14.1205C7.83953 14.0468 7.19582 13.1593 7.23082 13.0918C7.31581 12.9243 8.88323 11.9031 8.88323 11.9031L8.75199 11.7631L7.54455 11.4919L7.6183 10.9994C7.6183 10.9994 8.16077 10.5644 8.22576 10.5644C8.29076 10.5644 8.94073 10.4582 9.30196 10.3757C9.66319 10.2932 10.1069 10.1945 10.2132 10.137C10.3194 10.0795 10.3981 9.98072 10.3694 9.82448C10.3469 9.70698 10.0407 9.60324 10.0407 9.60324L6.87084 9.422L6.21962 9.49074C6.21962 9.49074 6.13213 9.78323 5.6634 10.0707C5.19467 10.3582 4.39722 10.5444 4.39722 10.5444L6.84709 12.0594Z" fill="#DC0D2A"/>
<path d="M3.62231 9.04443C3.62231 9.04443 4.55851 8.03324 5.05474 7.58951C5.42222 7.26078 5.7047 7.04829 5.8097 7.00829C5.86595 6.98704 6.40967 6.96829 6.46466 6.98454C6.52091 7.00079 6.57716 7.08829 6.48841 7.17703C6.40092 7.26453 6.12343 7.58201 5.87345 7.8445C5.75845 7.96449 5.53721 8.22323 5.53721 8.22323L5.12973 8.37447L4.10604 8.88569L3.62231 9.04443Z" fill="#AEE3FD"/>
<path d="M5.52591 8.22571C5.52591 8.22571 4.46222 8.40445 3.73976 8.94692C3.0173 9.48939 2.73731 10.5818 2.72106 10.6881C2.70481 10.7943 2.56732 11.1481 2.83856 11.318C2.9698 11.4005 3.23104 11.3618 3.50977 11.428C3.74101 11.4818 4.26473 11.658 4.66721 12.0193C5.06969 12.3805 5.78465 13.2267 5.78465 13.2267L8.75199 11.7643C8.75199 11.7643 7.89579 10.7631 5.91589 10.6318C3.936 10.5006 3.73101 10.5493 3.73101 10.5493C3.73101 10.5493 3.97975 9.83437 4.33098 9.3669C4.7997 8.74193 5.52591 8.22571 5.52591 8.22571Z" fill="#FE2A22"/>
<path d="M8.37714 13.2118C8.37714 13.2118 8.1859 12.6768 8.63587 12.3681C8.97585 12.1343 9.40708 12.1656 9.69957 12.5543C9.99205 12.943 9.73081 13.3993 9.57582 13.528C9.0696 13.9505 8.60837 13.5505 8.60837 13.5505C8.60837 13.5505 8.63837 13.7705 8.43963 13.9217C8.29839 14.0292 7.94466 14.0605 7.79842 13.7917C7.64093 13.5018 7.80217 13.2868 7.96716 13.1955C8.17965 13.0768 8.37714 13.2118 8.37714 13.2118Z" fill="#464C4F"/>
<path d="M3.04728 9.81194C3.04728 9.81194 3.721 9.80819 3.726 9.83569C3.731 9.86319 3.47851 10.5019 3.47851 10.5019C3.47851 10.5019 2.7073 10.9019 2.68355 10.8969C2.65981 10.8919 2.67855 10.5532 2.77855 10.2782C2.87854 10.0032 3.04728 9.81194 3.04728 9.81194Z" fill="#D9E3DF"/>
<path d="M6.21851 9.49316C6.21851 9.49316 6.80597 9.99064 7.86592 10.0806C8.92586 10.1706 10.0421 9.60441 10.0421 9.60441C10.0421 9.60441 8.25465 8.77695 8.03841 8.79195C7.80592 8.80695 6.22726 9.44316 6.21851 9.49316Z" fill="#FE2A22"/>
<path d="M3.32099 8.642C3.35223 8.75449 3.81971 9.10697 4.12344 9.07947C4.40218 9.05448 4.35468 8.53575 4.35218 8.40826C4.34843 8.15077 4.32593 7.93328 4.31093 7.79829C4.29468 7.6508 4.17219 7.63455 4.08595 7.70455C3.9997 7.77329 3.28724 8.52325 3.32099 8.642Z" fill="#464C4F"/>
</svg>

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,100 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.10094 15.5491L5.56352 16.2291L2.34369 8.94571L3.88111 8.26575L7.10094 15.5491Z" fill="url(#paint0_linear_746_959)"/>
<path d="M2.87744 9.64192C2.93369 9.52192 3.00868 9.43943 3.08743 9.36943C3.16743 9.30068 3.25367 9.24694 3.34492 9.20569C3.43741 9.16444 3.53491 9.13694 3.6399 9.12444C3.7449 9.11319 3.85614 9.11444 3.98238 9.15319C3.92614 9.27444 3.85239 9.35568 3.77239 9.42693C3.6924 9.49567 3.60615 9.54942 3.51366 9.59067C3.42116 9.63067 3.32367 9.65817 3.21867 9.67067C3.11618 9.68191 3.00493 9.68191 2.87744 9.64192Z" fill="#FFC107"/>
<path d="M3.73865 11.5881C3.79489 11.4681 3.86989 11.3856 3.94864 11.3156C4.02863 11.2469 4.11488 11.1931 4.20612 11.1519C4.29862 11.1106 4.39611 11.0831 4.50111 11.0706C4.6061 11.0594 4.71735 11.0606 4.84359 11.0994C4.78734 11.2206 4.7136 11.3018 4.6336 11.3731C4.5536 11.4418 4.46736 11.4956 4.37486 11.5368C4.28237 11.5768 4.18487 11.6043 4.07988 11.6168C3.97613 11.6281 3.86614 11.6268 3.73865 11.5881Z" fill="#FFC107"/>
<path d="M3.16492 10.2906C3.22116 10.1706 3.29616 10.0881 3.37491 10.0181C3.4549 9.94937 3.54115 9.89562 3.63239 9.85437C3.72489 9.81312 3.82238 9.78563 3.92738 9.77313C4.03237 9.76188 4.14362 9.76313 4.26986 9.80187C4.21361 9.92312 4.13987 10.0044 4.05987 10.0756C3.97987 10.1444 3.89363 10.1981 3.80113 10.2394C3.70864 10.2793 3.61114 10.3068 3.50615 10.3193C3.4024 10.3306 3.29241 10.3293 3.16492 10.2906Z" fill="#FFC107"/>
<path d="M4.31238 12.8854C4.36862 12.7655 4.44362 12.683 4.52237 12.613C4.60236 12.5442 4.68861 12.4905 4.77985 12.4492C4.87235 12.408 4.96984 12.3805 5.07484 12.368C5.17983 12.3567 5.28983 12.358 5.41732 12.3967C5.36107 12.518 5.28733 12.5992 5.20733 12.6705C5.12733 12.7392 5.04109 12.793 4.94859 12.8342C4.8561 12.8742 4.7586 12.9017 4.65361 12.9142C4.54987 12.9254 4.43987 12.9242 4.31238 12.8854Z" fill="#FFC107"/>
<path d="M4.0249 12.2368C4.08115 12.1168 4.15615 12.0343 4.23489 11.9643C4.31489 11.8955 4.40113 11.8418 4.49238 11.8005C4.58487 11.7593 4.68237 11.7318 4.78736 11.7193C4.89236 11.708 5.00235 11.7093 5.12984 11.748C5.0736 11.8693 4.99985 11.9505 4.91986 12.0218C4.83986 12.0905 4.75361 12.1443 4.66112 12.1855C4.56862 12.2255 4.47113 12.253 4.36613 12.2655C4.26239 12.2768 4.1524 12.2755 4.0249 12.2368Z" fill="#FFC107"/>
<path d="M5.1723 14.8316C5.2298 14.7116 5.30355 14.6291 5.38229 14.5591C5.46229 14.4904 5.54853 14.4366 5.63978 14.3954C5.73227 14.3541 5.82977 14.3266 5.93476 14.3141C6.03976 14.3029 6.14975 14.3041 6.27724 14.3429C6.221 14.4641 6.14725 14.5454 6.06726 14.6166C5.98726 14.6854 5.90101 14.7391 5.80977 14.7804C5.71727 14.8204 5.61978 14.8479 5.51478 14.8604C5.40979 14.8716 5.2998 14.8704 5.1723 14.8316Z" fill="#FFC107"/>
<path d="M3.45117 10.9394C3.50742 10.8194 3.58241 10.7369 3.66116 10.6669C3.74116 10.5982 3.8274 10.5444 3.91865 10.5032C4.01114 10.4619 4.10864 10.4344 4.21363 10.4219C4.31863 10.4107 4.42987 10.4119 4.55611 10.4507C4.49987 10.5719 4.42612 10.6532 4.34612 10.7244C4.26613 10.7932 4.17988 10.8469 4.08739 10.8882C3.99489 10.9282 3.8974 10.9557 3.7924 10.9682C3.68866 10.9794 3.57867 10.9781 3.45117 10.9394Z" fill="#FFC107"/>
<path d="M4.88611 14.1829C4.94236 14.0629 5.01735 13.9804 5.0961 13.9104C5.17609 13.8417 5.26234 13.788 5.35358 13.7467C5.44608 13.7055 5.54357 13.678 5.64857 13.6655C5.75356 13.6542 5.86356 13.6555 5.99105 13.6942C5.9348 13.8155 5.86106 13.8967 5.78106 13.9679C5.70107 14.0367 5.61482 14.0904 5.52357 14.1317C5.43108 14.1717 5.33358 14.1992 5.22859 14.2117C5.1236 14.2229 5.01235 14.2217 4.88611 14.1829Z" fill="#FFC107"/>
<path d="M4.59863 13.5343C4.65488 13.4143 4.72988 13.3318 4.80862 13.2618C4.88862 13.193 4.97486 13.1393 5.06611 13.098C5.1586 13.0568 5.2561 13.0293 5.36109 13.0168C5.46609 13.0055 5.57608 13.0068 5.70357 13.0455C5.64733 13.1668 5.57358 13.248 5.49359 13.3193C5.41359 13.388 5.32734 13.4418 5.2361 13.483C5.1436 13.523 5.04611 13.5505 4.94111 13.563C4.83612 13.5742 4.72613 13.573 4.59863 13.5343Z" fill="#FFC107"/>
<path d="M3.91738 8.48077L2.48995 9.11199C2.31746 9.18824 2.11747 9.11074 2.04123 8.93825C1.96498 8.76576 2.04248 8.56577 2.21497 8.48952L3.64239 7.85831C3.81488 7.78206 4.01487 7.85956 4.09112 8.03205C4.16737 8.20329 4.08987 8.40453 3.91738 8.48077Z" fill="#FFA000"/>
<g opacity="0.81">
<path opacity="0.81" d="M4.57732 13.9392L3.70612 11.993L5.09104 11.0693L6.16349 13.2905L4.57732 13.9392Z" fill="url(#paint1_linear_746_959)"/>
</g>
<path d="M2.11749 9.04568C2.23124 9.17068 2.42748 9.13568 2.42748 9.13568L2.48247 9.25942L4.02239 8.58571L3.96865 8.45821C3.96865 8.45821 4.13114 8.34697 4.11739 8.12573L2.11749 9.04568Z" fill="url(#paint2_linear_746_959)"/>
<path d="M2.81244 16.1078V16.4803H15.9867V16.1078C15.9867 13.9817 12.6482 12.9292 9.39959 12.933C6.17101 12.9367 2.81244 13.8554 2.81244 16.1078Z" fill="url(#paint3_linear_746_959)"/>
<path d="M3.73236 13.6642C3.73236 14.8329 5.57851 16.4803 5.57851 16.4803H13.0106C13.0106 16.4803 14.7205 14.7879 14.7205 13.408C14.7205 13.408 12.8244 12.8643 9.39956 12.933C5.57851 13.0093 3.73236 13.6642 3.73236 13.6642Z" fill="#212121"/>
<path d="M9.99328 13.3067C12.2157 13.3067 13.7056 13.5692 14.3106 13.7005C14.1418 14.5792 13.2969 15.6266 12.8494 16.1003H5.72476C5.14104 15.5554 4.33983 14.6229 4.15234 13.9355C4.8023 13.7592 6.53846 13.3705 9.40706 13.313C9.6033 13.3092 9.79954 13.3067 9.99328 13.3067ZM9.99328 12.9268C9.80079 12.9268 9.6033 12.928 9.39956 12.933C5.57976 13.0093 3.73236 13.6642 3.73236 13.6642C3.73236 14.8329 5.57851 16.4803 5.57851 16.4803H13.0106C13.0106 16.4803 14.7205 14.7879 14.7205 13.408C14.7205 13.408 13.0419 12.9268 9.99328 12.9268Z" fill="url(#paint4_linear_746_959)"/>
<path d="M7.55591 13.1867C7.55591 12.8142 7.58091 12.1593 8.02713 11.9505C8.33587 11.8068 10.4095 11.8805 10.7295 12.0455C11.1345 12.2543 11.2432 12.8342 11.2432 13.1855C11.2432 13.8154 10.4183 15.7028 9.40081 15.7028C8.38336 15.7028 7.55591 13.8167 7.55591 13.1867Z" fill="url(#paint5_linear_746_959)"/>
<path opacity="0.61" d="M7.67841 12.2467L7.59216 12.6592C7.59216 12.6592 8.23838 13.4404 9.39957 13.4404C10.7407 13.4404 11.1945 12.7604 11.1945 12.7604L11.0782 12.3742L7.67841 12.2467Z" fill="black"/>
<path d="M10.6057 16.4803C11.5294 15.4716 12.3882 14.2704 12.6444 13.0917L11.6744 12.8355C11.5519 12.803 11.427 12.873 11.3895 12.993C11.0282 14.1579 9.80079 15.4753 8.68335 16.4803H10.6057Z" fill="black"/>
<path d="M10.3483 16.4828C11.3132 15.4504 12.2307 14.2004 12.4969 12.9755L11.527 12.7193C11.4045 12.6868 11.2795 12.7568 11.242 12.8767C10.8657 14.0879 9.55332 15.4654 8.40088 16.4828H10.3483Z" fill="url(#paint6_linear_746_959)"/>
<g opacity="0.31">
<path opacity="0.31" d="M7.57971 12.9655C7.57971 12.9655 9.06213 15.1716 10.857 16.4278C10.857 16.4278 9.84834 16.544 9.81959 16.4803C9.79085 16.4165 7.65846 14.0841 7.65846 14.0841L7.57971 12.9655Z" fill="black"/>
</g>
<path d="M10.3333 16.4802C9.22584 15.4891 7.98215 14.1554 7.57968 12.9654C7.52718 12.8092 7.36219 12.7229 7.20595 12.7742L6.23975 13.1004C6.50473 14.2941 7.38344 15.4891 8.31964 16.4802H10.3333Z" fill="url(#paint7_linear_746_959)"/>
<path d="M9.39959 2.17358C7.19471 2.17358 5.15356 4.53096 5.15356 7.92453C5.15356 11.2994 7.2572 12.828 9.39959 12.828C11.542 12.828 13.6456 11.2994 13.6456 7.92453C13.6456 4.53096 11.6045 2.17358 9.39959 2.17358Z" fill="#F9DDBD"/>
<path d="M7.3172 8.86824C7.66167 8.86824 7.94092 8.57892 7.94092 8.22202C7.94092 7.86513 7.66167 7.57581 7.3172 7.57581C6.97273 7.57581 6.69348 7.86513 6.69348 8.22202C6.69348 8.57892 6.97273 8.86824 7.3172 8.86824Z" fill="#312D2D"/>
<path d="M11.482 8.86824C11.8265 8.86824 12.1057 8.57892 12.1057 8.22202C12.1057 7.86513 11.8265 7.57581 11.482 7.57581C11.1375 7.57581 10.8583 7.86513 10.8583 8.22202C10.8583 8.57892 11.1375 8.86824 11.482 8.86824Z" fill="#312D2D"/>
<path d="M8.25714 7.08204C8.13965 6.92704 7.86841 6.70081 7.34094 6.70081C6.81347 6.70081 6.54223 6.92704 6.42474 7.08204C6.37224 7.15078 6.38599 7.23078 6.42224 7.27828C6.45599 7.32327 6.55473 7.36452 6.66347 7.32702C6.77222 7.28952 6.98471 7.17953 7.34219 7.17703C7.69842 7.17953 7.91091 7.28952 8.0209 7.32702C8.12965 7.36452 8.22839 7.32327 8.26214 7.27828C8.29589 7.23078 8.30964 7.15078 8.25714 7.08204Z" fill="#454140"/>
<path d="M12.3744 7.08204C12.2569 6.92704 11.9857 6.70081 11.4582 6.70081C10.9307 6.70081 10.6595 6.92704 10.542 7.08204C10.4895 7.15078 10.5032 7.23078 10.5395 7.27828C10.5732 7.32327 10.672 7.36452 10.7807 7.32702C10.8895 7.28952 11.102 7.17953 11.4594 7.17703C11.8157 7.17953 12.0282 7.28952 12.1382 7.32702C12.2469 7.36452 12.3456 7.32327 12.3794 7.27828C12.4131 7.23078 12.4256 7.15078 12.3744 7.08204Z" fill="#454140"/>
<g opacity="0.17">
<path opacity="0.17" d="M9.39959 2.0061C7.05846 2.0061 5.15356 3.57852 5.15356 7.15208C5.15356 10.7456 6.99097 12.7105 9.39959 12.7105C11.8082 12.7105 13.8494 10.6994 13.8494 7.10458C13.8481 3.53102 11.7407 2.0061 9.39959 2.0061ZM9.39959 12.4106C7.82217 12.4106 5.73603 11.5981 5.73603 8.657C5.73603 5.73216 7.48969 5.81215 9.39959 5.81215C11.3095 5.81215 13.0631 5.73216 13.0631 8.657C13.0631 11.5981 10.977 12.4106 9.39959 12.4106Z" fill="black"/>
</g>
<path d="M9.44585 8.60449C10.8395 8.60449 13.3319 9.94067 13.3319 9.94067C13.2557 11.2181 11.592 12.688 9.5346 12.658C6.08228 12.608 5.46606 10.0432 5.46606 10.0432C5.46606 10.0432 8.05218 8.60449 9.44585 8.60449Z" fill="url(#paint8_linear_746_959)"/>
<path opacity="0.4" d="M13.3281 9.99189C13.3281 9.98814 13.3294 9.98439 13.3294 9.98189C13.1969 9.90314 13.0632 9.82065 12.9282 9.73565C12.1645 9.36067 10.4945 8.60571 9.4471 8.60571C8.48215 8.60571 6.85473 9.31943 5.85479 9.84315C5.6548 9.97689 5.76979 9.98939 5.57605 10.1119C6.23102 9.8819 8.11842 9.02944 9.41835 9.02944C10.6945 9.02819 12.6294 9.7669 13.3281 9.99189Z" fill="#BDBDBD"/>
<path d="M9.39959 1.49988C6.95596 1.49988 4.55609 3.26728 4.55609 6.8421C4.55609 10.4357 6.88472 12.933 9.39834 12.933C11.912 12.933 14.2406 10.4357 14.2406 6.8421C14.2418 3.26728 11.8432 1.49988 9.39959 1.49988ZM9.39959 12.6555C7.75217 12.6555 5.57604 11.0919 5.57604 8.15078C5.57604 5.22593 7.40594 5.30593 9.39959 5.30593C11.3932 5.30593 13.2231 5.22593 13.2231 8.15078C13.2231 11.0906 11.0457 12.6555 9.39959 12.6555Z" fill="url(#paint9_linear_746_959)"/>
<path d="M9.53456 2.71479C9.53456 2.71479 7.72966 2.15357 6.08225 3.92723C4.50233 5.62839 4.9948 8.87947 4.9948 8.87947C4.9948 8.87947 4.82731 6.04587 6.69221 4.27221C8.32338 2.71979 9.53456 2.71479 9.53456 2.71479Z" fill="url(#paint10_linear_746_959)"/>
<path d="M12.5669 5.14215C13.6194 6.1646 13.6344 8.88946 13.6344 8.88946C13.6344 8.88946 14.4231 6.14585 12.9781 4.66718C11.7832 3.44349 10.1283 3.93847 10.1283 3.93847C10.1283 3.93847 11.4582 4.06471 12.5669 5.14215Z" fill="url(#paint11_linear_746_959)"/>
<g opacity="0.38">
<path opacity="0.38" d="M9.39959 1.74986C10.612 1.74986 11.7357 2.19734 12.5619 3.00855C13.4981 3.92725 13.9918 5.25218 13.9918 6.8421C13.9918 8.38826 13.5394 9.81819 12.7144 10.8969C13.1744 10.1707 13.4731 9.25197 13.4731 8.15203C13.4731 6.8021 13.0756 5.92589 12.2569 5.47092C11.5682 5.08844 10.6895 5.05719 9.69582 5.05719H9.54833H9.40084H9.25334H9.10585C7.42219 5.05719 5.3273 5.05719 5.3273 8.15203C5.3273 9.25322 5.62478 10.1707 6.08601 10.8969C5.26105 9.81819 4.80858 8.38826 4.80858 6.8421C4.80858 5.25343 5.30355 3.92725 6.2385 3.00855C7.06346 2.19734 8.18715 1.74986 9.39959 1.74986ZM9.39959 1.49988C6.95596 1.49988 4.55609 3.26728 4.55609 6.8421C4.55609 10.4357 6.88472 12.933 9.39834 12.933C11.912 12.933 14.2406 10.4357 14.2406 6.8421C14.2418 3.26728 11.8432 1.49988 9.39959 1.49988ZM9.39959 5.30593C9.49833 5.30593 9.59707 5.30593 9.69457 5.30593C11.4982 5.30593 13.2231 5.37717 13.2231 8.15078C13.2231 11.0906 10.9733 12.6555 9.39959 12.6555C7.82592 12.6555 5.57604 11.0906 5.57604 8.15078C5.57604 5.37717 7.30095 5.30593 9.1046 5.30593C9.2021 5.30593 9.30084 5.30593 9.39959 5.30593Z" fill="url(#paint12_linear_746_959)"/>
</g>
<defs>
<linearGradient id="paint0_linear_746_959" x1="3.42906" y1="9.47632" x2="5.21624" y2="13.306" gradientUnits="userSpaceOnUse">
<stop stop-color="#646464"/>
<stop offset="1" stop-color="#444444"/>
</linearGradient>
<linearGradient id="paint1_linear_746_959" x1="5.47577" y1="13.9528" x2="4.50005" y2="11.5885" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_746_959" x1="3.28571" y1="8.99385" x2="3.11769" y2="8.58672" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint3_linear_746_959" x1="9.39947" y1="16.6913" x2="9.39947" y2="15.0654" gradientUnits="userSpaceOnUse">
<stop offset="0.1696" stop-color="#646464"/>
<stop offset="1" stop-color="#757575"/>
</linearGradient>
<linearGradient id="paint4_linear_746_959" x1="9.22613" y1="12.927" x2="9.22613" y2="16.4801" gradientUnits="userSpaceOnUse">
<stop stop-color="#646464"/>
<stop offset="0.2649" stop-color="#575757"/>
<stop offset="0.7538" stop-color="#353535"/>
<stop offset="1" stop-color="#212121"/>
</linearGradient>
<linearGradient id="paint5_linear_746_959" x1="9.39944" y1="12.6796" x2="9.39944" y2="15.4456" gradientUnits="userSpaceOnUse">
<stop offset="0.0744" stop-color="#444444"/>
<stop offset="0.653" stop-color="#212121"/>
</linearGradient>
<linearGradient id="paint6_linear_746_959" x1="11.6532" y1="13.1431" x2="10.2698" y2="15.2902" gradientUnits="userSpaceOnUse">
<stop stop-color="#646464"/>
<stop offset="1" stop-color="#444444"/>
</linearGradient>
<linearGradient id="paint7_linear_746_959" x1="8.38795" y1="15.2569" x2="6.35194" y2="12.3334" gradientUnits="userSpaceOnUse">
<stop stop-color="#646464"/>
<stop offset="1" stop-color="#9E9E9E"/>
</linearGradient>
<linearGradient id="paint8_linear_746_959" x1="9.39942" y1="9.89264" x2="9.39942" y2="12.9208" gradientUnits="userSpaceOnUse">
<stop offset="0.0744" stop-color="#444444"/>
<stop offset="0.653" stop-color="#212121"/>
</linearGradient>
<linearGradient id="paint9_linear_746_959" x1="9.39934" y1="1.56391" x2="9.39934" y2="12.7995" gradientUnits="userSpaceOnUse">
<stop offset="0.0744" stop-color="#444444"/>
<stop offset="0.653" stop-color="#212121"/>
</linearGradient>
<linearGradient id="paint10_linear_746_959" x1="5.59864" y1="6.32337" x2="8.30122" y2="2.98737" gradientUnits="userSpaceOnUse">
<stop stop-color="#0D0C0C"/>
<stop offset="1" stop-color="#0D0C0C" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint11_linear_746_959" x1="12.9059" y1="6.30033" x2="11.0478" y2="4.10448" gradientUnits="userSpaceOnUse">
<stop stop-color="#0D0C0C"/>
<stop offset="1" stop-color="#0D0C0C" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint12_linear_746_959" x1="9.39934" y1="1.49988" x2="9.39934" y2="12.933" gradientUnits="userSpaceOnUse">
<stop stop-color="#BDBDBD"/>
<stop offset="1" stop-color="#646464"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,14 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.7946 16.0455C8.7946 16.0455 13.3519 15.573 14.2118 14.8205C14.6418 14.4443 15.1305 13.6406 15.4155 13.1656C15.7005 12.6894 15.888 12.3482 16.0605 11.8757C16.4042 10.9295 16.5767 8.09214 16.0605 6.67347C15.5442 5.25479 14.878 3.98611 13.9531 3.23365C13.0444 2.49119 11.4382 1.48999 8.98709 1.48999C7.8934 1.48999 7.06469 1.71998 6.36473 1.96247C5.31728 2.3262 4.72231 2.72368 4.32234 3.05866C3.84236 3.46239 2.89241 4.29609 2.23745 5.78976C1.98121 6.37223 1.69872 7.18844 1.61373 7.9184C1.31249 10.5195 1.78497 12.7132 3.28989 14.2606C4.79481 15.808 5.35353 15.593 5.35353 15.593L8.7946 16.0455Z" fill="#F47A00"/>
<path d="M4.90973 14.103C4.90973 14.103 4.6735 14.103 4.39351 13.9743C4.11353 13.8456 3.08233 12.4044 2.88859 11.8457C2.69485 11.2869 2.25363 9.31955 2.45861 8.3846C2.57611 7.85088 2.7161 7.4384 2.7161 7.4384C2.7161 7.4384 3.23607 6.03597 3.30107 5.58475C3.36607 5.13352 3.94229 4.6223 4.22102 4.29981C4.49976 3.97733 4.6935 3.65485 5.20972 3.35361C5.72594 3.05238 6.26341 3.05238 6.60715 2.88114C6.95088 2.70865 7.46085 2.39741 7.96207 2.25742C8.91327 1.99119 9.48824 2.27867 10.0045 2.25742C10.5207 2.23617 11.0094 2.39866 11.6431 2.73865C12.0944 2.98113 12.4431 3.25987 12.8506 3.58235C13.258 3.90483 13.8243 4.09857 14.2392 4.62105C14.6367 5.12102 14.7117 5.69599 14.9492 6.34096C15.1854 6.98592 15.4454 7.3259 15.4892 7.75588C15.5317 8.18586 15.4942 8.52334 15.5154 9.01707C15.5367 9.51079 15.6467 9.88077 15.6217 10.1683C15.5692 10.7882 15.3317 10.9245 15.2679 11.3544C15.2029 11.7844 14.928 12.2869 14.7892 12.5606C14.4142 13.3018 13.7255 13.7031 13.4355 13.8956C12.9318 14.2343 4.90973 14.103 4.90973 14.103Z" fill="#FFA726"/>
<path d="M3.13976 8.74452C2.69603 8.68577 2.29105 9.13949 2.3198 9.67322C2.3498 10.2069 2.70478 10.3844 3.08976 10.2857C3.57724 10.1607 3.75098 9.71321 3.74098 9.38698C3.73098 9.06075 3.58224 8.80326 3.13976 8.74452Z" fill="#F27B00"/>
<path d="M5.95848 5.41845C5.56975 5.10347 5.71225 4.67474 5.92224 4.40225C6.13222 4.12977 6.71094 4.00353 6.95218 4.4235C7.19342 4.84348 7.01343 5.13597 6.82219 5.33596C6.6347 5.53344 6.21722 5.62844 5.95848 5.41845Z" fill="#F27B00"/>
<path d="M7.60341 4.27854C7.7709 4.61477 8.24462 4.67352 8.56086 4.49603C8.87709 4.31854 8.98583 3.97231 8.79834 3.65732C8.61335 3.34609 8.13713 3.2036 7.7909 3.47983C7.44592 3.75482 7.50466 4.08105 7.60341 4.27854Z" fill="#F27B00"/>
<path d="M9.54186 5.50722C9.69561 5.8622 10.1331 5.9197 10.4718 5.75096C10.8105 5.58222 11.0493 5.13099 10.8468 4.78226C10.6431 4.43478 10.1868 4.40353 9.8831 4.61352C9.57936 4.82351 9.37062 5.11224 9.54186 5.50722Z" fill="#F27B00"/>
<path d="M11.397 4.59352C11.6807 4.82101 12.1757 4.68977 12.3569 4.35478C12.4956 4.09855 12.5719 3.80356 12.2857 3.51733C11.9994 3.23109 11.5319 3.35859 11.317 3.66107C11.1482 3.90106 11.1007 4.35728 11.397 4.59352Z" fill="#F27B00"/>
<path d="M4.47722 6.2734C4.26473 6.2884 4.29598 6.72463 4.28598 7.3471C4.27598 7.94207 4.15849 9.19075 4.22474 10.5307C4.25223 11.0944 4.19349 12.6843 4.24723 13.483C4.30223 14.283 4.26973 15.1767 4.26973 15.1767C4.26973 15.1767 5.55092 16.5041 8.99448 16.5541C12.6218 16.6066 14.1017 14.8717 14.1017 14.8717C14.1017 14.8717 14.0617 12.6831 14.0655 11.8219C14.0705 10.5482 13.9917 8.12581 13.9917 7.73458C13.9917 7.34335 14.0242 6.4209 13.883 6.2959C13.7417 6.17091 12.738 6.22716 11.8231 6.24966C11.0044 6.26965 8.14828 6.27715 7.09833 6.27715C6.04839 6.27715 4.91595 6.24216 4.47722 6.2734Z" fill="#3C3C3C"/>
<path d="M4.96974 7.12343C4.83599 7.20467 4.81349 8.28212 4.84474 9.28581C4.87599 10.2895 4.87599 11.2445 5.20472 11.307C5.53346 11.3695 5.86219 10.6332 6.20717 10.3508C6.55215 10.0683 6.95963 9.84953 7.22587 9.4583C7.4921 9.06707 7.78959 8.43961 7.93083 8.12712C8.07207 7.81339 8.5108 7.39091 8.4633 7.13968C8.4158 6.88844 6.88088 6.98343 6.41091 6.98343C5.94093 6.98343 5.25222 6.95219 4.96974 7.12343Z" fill="#858585"/>
<path d="M9.34083 6.96717C9.14209 7.10841 8.93335 7.65588 8.79211 7.90712C8.65087 8.15836 8.25964 8.70583 8.55712 8.89457C8.85461 9.08206 9.44957 8.64333 9.63831 8.51834C9.8258 8.39335 10.077 8.33085 10.1708 8.2521C10.2645 8.17336 10.7345 7.48464 10.8445 7.37465C10.9545 7.26466 11.1895 7.13966 11.1895 7.01467C11.1895 6.88967 10.6095 6.88968 10.0933 6.90467C9.57707 6.91967 9.45082 6.88843 9.34083 6.96717Z" fill="#858585"/>
<path d="M7.53947 10.147C7.39448 10.3445 7.14824 10.7733 7.42948 10.977C7.71196 11.1807 8.02445 10.9607 8.24444 10.7258C8.46442 10.4908 8.57317 10.177 8.30693 9.8958C8.0407 9.61456 7.71071 9.91205 7.53947 10.147Z" fill="#858585"/>
<path d="M6.5997 11.2757C6.44346 11.2632 6.11347 11.762 5.81599 11.887C5.51851 12.012 5.06353 11.9345 4.98603 12.2157C4.90729 12.4982 4.7348 13.6881 4.98603 13.8294C5.23727 13.9706 5.92598 13.7356 6.08223 13.6419C6.23847 13.5481 6.22347 13.3281 6.53595 13.1244C6.84969 12.9207 6.89593 12.3882 6.91218 12.012C6.92843 11.6357 6.80219 11.2907 6.5997 11.2757Z" fill="#858585"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,21 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.2395 13.9305C13.2395 13.9305 13.2283 15.2642 13.2283 15.5292C13.2283 15.7941 13.3783 15.9316 13.5508 15.9204C13.7232 15.9091 14.552 15.9204 14.7357 15.9204C14.9194 15.9204 15.1269 15.7479 15.1269 15.4954C15.1269 15.2429 15.1619 12.988 15.1619 12.988L13.2395 13.9305Z" fill="#4E433D"/>
<path d="M4.42997 13.1492L2.60132 12.8967C2.60132 12.8967 2.59007 15.2316 2.57882 15.4503C2.56757 15.6691 2.69381 15.8278 2.87756 15.8416C3.20004 15.8641 3.80876 15.8641 4.02749 15.8528C4.24623 15.8416 4.39497 15.6803 4.40747 15.4728C4.41997 15.2654 4.40747 14.0242 4.40747 14.0242L4.42997 13.1492Z" fill="#4E433D"/>
<path d="M14.0606 8.08963L15.2093 8.03838C15.2093 8.03838 15.638 8.71959 15.5843 11.0245C15.533 13.2131 14.9681 14.0681 14.4681 14.1893C13.9681 14.3105 12.0807 14.4968 9.09088 14.5143C6.10103 14.5318 3.8799 14.3605 3.45242 14.2581C3.02495 14.1556 2.56247 13.5231 2.51247 13.0619C2.43623 12.3544 2.43123 11.4032 2.46497 10.0533C2.49872 8.70334 2.79246 8.02588 2.79246 8.02588L14.0606 8.08963Z" fill="#474C4F"/>
<path d="M4.35115 11.8981C3.70369 11.7906 2.76374 11.3831 2.69249 11.6831C2.62125 11.9831 2.90748 12.3406 3.5937 12.5693C4.27991 12.7981 5.85233 12.9693 9.15465 12.8981C12.457 12.8268 13.5719 12.7406 14.2294 12.5556C14.8868 12.3693 15.6443 11.8694 15.4018 11.5831C15.1593 11.2969 14.5156 11.7119 13.9294 11.8256C13.3432 11.9394 11.1408 12.0006 9.11215 12.0406C6.91102 12.0831 5.03737 12.0118 4.35115 11.8981Z" fill="#858585"/>
<path d="M5.57114 10.0206C5.3574 9.96435 2.70504 9.9031 2.58879 10.0281C2.53755 10.0831 2.55005 10.8793 2.56004 11.0243C2.57004 11.1693 2.68129 11.2193 2.87378 11.2293C3.06752 11.2393 5.54239 11.258 5.70613 11.2393C5.86987 11.2205 5.90862 11.0555 5.87987 10.9105C5.85112 10.7643 5.75488 10.0693 5.57114 10.0206Z" fill="#FEA826"/>
<path d="M15.3544 9.89699C15.2331 9.82574 12.542 9.93324 12.4558 10.0107C12.3683 10.0882 11.902 11.0507 12.137 11.1419C12.3595 11.2294 15.2681 11.1832 15.3644 11.1344C15.5143 11.0594 15.4694 9.96449 15.3544 9.89699Z" fill="#FEA826"/>
<path d="M6.95345 11.4419C6.69221 11.4544 6.32473 11.3156 6.16974 10.9107C6.01475 10.5044 5.996 10.2632 6.0435 10.0507C6.09224 9.83822 6.23724 9.76072 6.57472 9.73198C6.91345 9.70323 7.57092 9.65448 8.98209 9.64448C10.3945 9.63448 11.3707 9.66323 11.622 9.69323C11.8732 9.72198 12.0182 9.80947 11.9794 10.0707C11.9407 10.3319 11.8057 10.8544 11.6994 11.1057C11.622 11.2894 11.3807 11.4244 11.0607 11.4344C10.7432 11.4419 7.15719 11.4319 6.95345 11.4419Z" fill="#2F2F2F"/>
<path d="M14.668 7.41965L15.0442 7.42965C15.0442 7.42965 15.0529 6.97717 15.1042 6.83718C15.1554 6.69719 15.2592 6.62219 15.4942 6.62219C15.7292 6.62219 16.1904 6.63969 16.3454 6.64719C16.5004 6.65469 16.8016 6.91968 16.8166 7.31716C16.8316 7.71464 16.5591 7.96462 16.2866 7.99462C16.0141 8.02462 14.9917 7.99462 14.9917 7.99462L14.668 7.41965Z" fill="#474C4F"/>
<path d="M3.45106 7.35714L3.03358 7.38339C3.03358 7.38339 3.04608 6.97466 3.01733 6.82717C2.98858 6.67968 2.81109 6.59219 2.6786 6.58469C2.54611 6.57719 1.92739 6.57719 1.75115 6.59968C1.57491 6.62218 1.27242 6.79842 1.27992 7.24715C1.28742 7.69588 1.57491 7.94586 1.81739 7.93961C2.05988 7.93336 2.64735 7.95211 2.82609 7.94711C3.05483 7.93961 3.5548 7.96211 3.5548 7.96211L3.45106 7.35714Z" fill="#474C4F"/>
<path d="M7.95972 3.09839L7.97847 3.68711H10.3571L10.3758 3.11714L7.95972 3.09839Z" fill="#474C4F"/>
<path d="M11.6233 3.07858C11.6233 3.22357 11.6233 3.84229 11.6233 3.84229L12.407 3.86104L12.3583 3.04858L11.6233 3.07858Z" fill="#474C4F"/>
<path d="M5.73486 3.83223C5.75361 3.71598 5.76361 2.89478 5.76361 2.89478L6.60482 3.01102L6.57607 3.76473L5.73486 3.83223Z" fill="#474C4F"/>
<path d="M2.7888 8.02582C2.81005 7.98582 3.06754 7.89458 3.22253 7.62209C3.33128 7.43085 3.64751 6.40341 4.06374 5.5822C4.47996 4.76099 4.65371 4.40226 4.76995 4.21852C4.88619 4.03478 5.05993 3.88979 5.35992 3.80229C5.6599 3.7148 6.70485 3.47856 9.21846 3.52731C11.7321 3.57606 12.3695 3.65105 12.6145 3.7298C12.812 3.79229 13.182 3.98603 13.4232 4.43101C13.6645 4.87599 14.6882 7.15962 14.7857 7.36211C14.8819 7.5646 15.2106 8.03582 15.2106 8.03582C15.2106 8.03582 14.3419 8.93952 12.3121 9.17451C10.4871 9.38575 7.83729 9.57449 5.10118 9.10202C2.9538 8.73453 2.76756 8.06582 2.7888 8.02582Z" fill="#E3E4DE"/>
<path d="M5.01991 4.55725C4.79743 4.77974 4.2962 5.87968 4.04747 6.51215C3.79873 7.14461 3.65249 7.66458 3.99122 7.67583C4.32995 7.68708 7.55103 7.64209 8.97596 7.63084C10.4009 7.61959 13.802 7.69833 14.0507 7.69833C14.2994 7.69833 14.4457 7.48334 14.3332 7.21211C14.2207 6.94087 13.3832 5.08722 13.192 4.78224C12.9995 4.47725 12.7283 4.37476 12.2533 4.35226C11.7783 4.32976 10.0034 4.22852 8.78347 4.22852C7.56353 4.22852 5.22365 4.35476 5.01991 4.55725Z" fill="#2D2E35"/>
<path d="M4.26346 7.15069C4.31221 7.22444 7.3708 7.0907 9.2132 7.12444C11.0556 7.15819 13.5192 7.21444 13.6442 7.20319C13.768 7.19194 13.7805 7.0107 13.768 6.95445C13.7567 6.89821 13.5417 6.45698 13.5417 6.45698C13.5417 6.45698 12.423 5.1908 12.3318 5.1908C12.2418 5.1908 9.07696 5.39454 9.07696 5.39454L5.51715 5.29329L4.44345 6.61822C4.44345 6.61822 4.24096 7.11694 4.26346 7.15069Z" fill="#96C8ED"/>
<path d="M4.4436 6.61844C4.4436 6.61844 4.94233 5.42475 5.14357 5.12227C5.22231 5.00478 5.27981 4.89603 5.55105 4.86228C5.82228 4.82853 7.22346 4.6248 9.09961 4.65854C10.9758 4.69229 12.0607 4.76729 12.2869 4.78229C12.5544 4.79979 12.7394 4.89478 12.8407 5.06477C12.9419 5.23476 13.6544 6.69219 13.6544 6.69219L12.5044 6.58719C12.5044 6.58719 12.4969 6.05722 12.4794 5.87473C12.4644 5.71724 12.3907 5.58724 12.1494 5.5735C11.9345 5.56225 11.4595 5.551 11.302 5.56225C11.1432 5.5735 11.0358 5.70474 11.0045 5.92973C10.9733 6.15596 10.9408 6.55969 10.9408 6.55969L7.3797 6.58094C7.3797 6.58094 7.3797 6.02347 7.3472 5.82223C7.32345 5.67724 7.23221 5.58224 7.04597 5.58349C6.82973 5.58474 6.41975 5.56225 6.12601 5.59599C5.98977 5.61224 5.88853 5.77723 5.87728 6.01472C5.86853 6.18596 5.85478 6.56844 5.85478 6.56844L4.4436 6.61844Z" fill="#AFE3FB"/>
<path d="M8.05607 1.98621H10.3285L10.7434 2.73117L10.3085 3.27239H8.05607L7.62109 2.59492L8.05607 1.98621Z" fill="#CECECF"/>
<path d="M10.3283 1.98621C10.3283 1.98621 12.2807 2.01495 12.4169 2.03495C12.5532 2.05495 12.6882 2.21869 12.7069 2.41243C12.7257 2.60617 12.6882 3.0799 12.6882 3.16614C12.6882 3.25239 12.5769 3.30489 12.3944 3.30489C12.2107 3.30489 10.3108 3.27239 10.3108 3.27239L10.3283 1.98621Z" fill="#DD0C26"/>
<path d="M5.42603 3.02115C5.42603 3.1474 5.43603 3.24364 5.56102 3.25364C5.68726 3.26364 8.05589 3.27239 8.05589 3.27239V1.98621C8.05589 1.98621 5.89975 1.98621 5.74476 1.98621C5.58977 1.98621 5.42603 2.16995 5.42603 2.31494C5.42603 2.51743 5.42603 3.02115 5.42603 3.02115Z" fill="#0F64C7"/>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1,15 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.7991 15.0355C15.7317 14.6755 15.4717 12.6843 15.4717 12.6843H2.52735C2.52735 12.6843 2.26736 14.6755 2.19987 15.0355C2.13237 15.3954 2.90358 16.4991 8.99951 16.4991C15.0954 16.4991 15.8666 15.3954 15.7991 15.0355Z" fill="#82AEC0"/>
<path d="M2.52368 12.7445C2.52368 12.0107 5.42353 11.4158 8.99959 11.4158C12.5757 11.4158 15.4755 12.0107 15.4755 12.7445C15.4755 13.4782 12.5757 14.1944 8.99959 14.1944C5.42353 14.1944 2.52368 13.4782 2.52368 12.7445Z" fill="#E0E0E0"/>
<path d="M13.2217 3.03992C12.8667 1.90873 11.6843 1.5 8.99816 1.5C6.31205 1.5 5.13087 1.90873 4.77589 3.03992C4.06092 5.31855 3.41846 12.5032 3.41846 12.5032C3.41846 12.5032 3.97718 13.4394 8.99941 13.4394C14.0216 13.4394 14.5816 12.5044 14.5816 12.5044C14.5816 12.5044 13.9367 5.31855 13.2217 3.03992Z" fill="#F44336"/>
<path opacity="0.9" d="M3.41724 12.517C3.41724 11.9421 5.91585 11.4758 8.99944 11.4758C12.083 11.4758 14.5816 11.9421 14.5816 12.517C14.5816 13.092 12.083 13.6532 8.99944 13.6532C5.91585 13.6532 3.41724 13.092 3.41724 12.517Z" fill="#C62828"/>
<path d="M10.0731 5.46596L12.8017 2.64986C12.8542 2.60361 12.9317 2.66236 12.9017 2.72486L10.8718 5.98218C10.7206 6.29717 10.8543 6.50091 11.2043 6.49216L13.7854 6.46591C13.8554 6.46466 13.8767 6.55965 13.8129 6.5884L11.4581 7.49585C11.1393 7.6396 11.0718 7.98083 11.3443 8.19832L14.2804 10.3157C14.3354 10.3595 14.2904 10.4457 14.2229 10.427L10.8818 9.04327C10.5456 8.94828 10.3919 9.13452 10.4606 9.477L11.5381 13.0881C11.5518 13.1568 11.4618 13.1943 11.4231 13.1368L9.58441 9.88073C9.38817 9.59199 9.15068 9.537 8.98444 9.84323L7.22079 13.1781C7.18704 13.2393 7.09454 13.2106 7.10079 13.1406L8.01199 9.542C8.04699 9.19452 7.887 9.12202 7.56202 9.24951L3.78097 11.1882C3.71597 11.2132 3.66347 11.1319 3.71347 11.0832L6.8858 8.44955C7.13579 8.20582 7.12454 7.93958 6.79331 7.82834L4.14595 6.92588C4.0797 6.90339 4.0922 6.80714 4.16095 6.80214L6.76581 6.85214C7.11454 6.82589 7.29078 6.47841 7.10829 6.18092L5.01465 3.16233C4.9784 3.10234 5.04965 3.03609 5.1059 3.07734L7.89825 5.50221C8.18199 5.70595 8.43072 5.6872 8.48197 5.34222L8.9357 2.09239C8.9457 2.02364 9.04319 2.01864 9.06069 2.08614L9.45567 5.25847C9.54316 5.5972 9.81065 5.6972 10.0731 5.46596Z" fill="url(#paint0_radial_716_535)"/>
<path d="M9.23063 5.25724L10.4006 3.1461C10.4356 3.08611 10.5281 3.11735 10.5193 3.1861L10.1543 5.49223C10.1118 5.83846 10.3043 5.9897 10.6318 5.86971L12.9229 5.09975C12.9879 5.076 13.0392 5.1585 12.9879 5.20599L11.193 6.73841C10.9368 6.9759 10.9818 7.32088 11.3105 7.43963L13.6004 8.21584C13.6654 8.23958 13.6516 8.33583 13.5816 8.33958L11.1418 8.38833C10.793 8.40583 10.7068 8.63206 10.8818 8.93455L12.1142 11.0394C12.1492 11.0994 12.0755 11.1644 12.0205 11.1219L10.1793 9.59701C9.90059 9.38702 9.65936 9.40952 9.59936 9.75325L9.16188 12.2456C9.14938 12.3144 9.05189 12.3169 9.03689 12.2494L8.58191 9.77825C8.50442 9.43827 8.32943 9.42077 8.06194 9.64451L6.12454 11.2732C6.0708 11.3182 5.99455 11.2569 6.02705 11.1957L7.16574 9.10329C7.32448 8.79206 7.22948 8.54457 6.8795 8.54457L4.42213 8.57582C4.35214 8.57582 4.33339 8.48082 4.39713 8.45332L6.54202 7.62712C6.86325 7.49087 6.9195 7.10589 6.65201 6.88091L4.85211 5.41473C4.79836 5.36974 4.84586 5.28474 4.91211 5.30474L7.18324 5.9847C7.51697 6.0872 7.74696 5.9897 7.68571 5.64597L7.21448 3.26985C7.20199 3.2011 7.29323 3.1661 7.33073 3.2236L8.58191 5.25599C8.77065 5.54973 9.05689 5.55973 9.23063 5.25724Z" fill="#FFD5CA"/>
<defs>
<radialGradient id="paint0_radial_716_535" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8.99962 7.62467) scale(5.44854)">
<stop offset="0.5718" stop-color="#FF6E40"/>
<stop offset="0.7682" stop-color="#FF7046" stop-opacity="0.5414"/>
<stop offset="1" stop-color="#FF7555" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -55,7 +55,6 @@ export function useCreateExportDashboard({
layouts: [],
panels: {},
variables: [],
links: [],
},
}),
{

View File

@@ -8,6 +8,11 @@
.dashboardImage {
flex-shrink: 0;
// Fixed 16px box (matches the breadcrumb icon); object-fit keeps a custom
// base64 icon from distorting or ballooning past it.
width: 16px;
height: 16px;
object-fit: contain;
}
.dashboardTitle {

View File

@@ -34,7 +34,7 @@ const dashboard = {
id: 'dash-1',
name: 'My dashboard',
schemaVersion: 'v6',
image: 'icon.png',
image: '/assets/Icons/eight-ball',
tags: [{ key: 'env', value: 'prod' }],
spec: {
display: { name: 'My dashboard' },
@@ -77,7 +77,7 @@ describe('useJsonEditor', () => {
const parsed = JSON.parse(result.current.draft);
// Key order is intentional: spec, then tags, then image.
expect(Object.keys(parsed)).toStrictEqual(['spec', 'tags', 'image']);
expect(parsed.image).toBe('icon.png');
expect(parsed.image).toBe('/assets/Icons/eight-ball');
expect(parsed.id).toBeUndefined();
expect(parsed.name).toBeUndefined();
expect(parsed.schemaVersion).toBeUndefined();
@@ -167,7 +167,7 @@ describe('useJsonEditor', () => {
// preserved from the original dashboard (redacted from the editor)
name: 'My dashboard',
schemaVersion: 'v6',
image: 'icon.png',
image: '/assets/Icons/eight-ball',
// edited via the draft
spec: editedSpec,
tags: editedTags,

View File

@@ -13,6 +13,7 @@ import { toAPIError } from 'utils/errorUtils';
import { dashboardToUpdatable } from './dashboardToUpdatable';
import { findPanelLayoutIssues } from './danglingPanels';
import { compactSpecLayouts } from '../../layoutCompaction';
import { isValidDashboardImage } from '../../dashboardIcons';
import { useDashboardStore } from '../../store/useDashboardStore';
export interface JsonValidity {
@@ -108,9 +109,9 @@ export function useJsonEditor({
const validity = useMemo<JsonValidity>(() => {
const lineCount = draft.split('\n').length;
let parsed: { image?: unknown };
try {
JSON.parse(draft);
return { valid: true, lineCount };
parsed = JSON.parse(draft);
} catch (error) {
const message = error instanceof Error ? error.message : 'Invalid JSON';
return {
@@ -120,6 +121,21 @@ export function useJsonEditor({
message,
};
}
// `image` only renders for a system icon / logo path or a base64 image, so
// reject anything else (URLs, markup, …) at save rather than persist it.
const { image } = parsed;
if (
image !== undefined &&
(typeof image !== 'string' || !isValidDashboardImage(image))
) {
return {
valid: false,
lineCount,
message:
'"image" must be an /assets/Icons/<name> or /assets/Logos/<name> path, or a base64 image data URI',
};
}
return { valid: true, lineCount };
}, [draft]);
const isDirty = draft !== appliedText;

View File

@@ -13,7 +13,7 @@ import type {
DashboardtypesJSONPatchOperationDTO,
GetDashboardV2200,
} from 'api/generated/services/sigNoz.schemas';
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
import { resolveDashboardImage } from 'pages/DashboardPageV2/DashboardContainer/dashboardIcons';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
import { useAppContext } from 'providers/App/App';
@@ -55,7 +55,7 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
const title = dashboard.spec.display.name;
const description = dashboard.spec.display.description ?? '';
const image = dashboard.image || Base64Icons[0];
const image = resolveDashboardImage(dashboard.image);
const tags = useMemo(
() =>
(dashboard.tags ?? []).map((t) =>

View File

@@ -1,4 +1,3 @@
import { useEffect, useMemo, useState } from 'react';
import {
Select,
SelectContent,
@@ -6,13 +5,16 @@ import {
SelectTrigger,
} from '@signozhq/ui/select';
import cx from 'classnames';
import { Base64Icons } from '../utils';
import {
resolveDashboardImage,
SYSTEM_ICON_PATHS,
} from 'pages/DashboardPageV2/DashboardContainer/dashboardIcons';
import styles from './DashboardImagePicker.module.scss';
interface Props {
// The selected image — one of the base64 icon data-URIs.
// The selected image — a system icon path (`/assets/Icons/<name>`) or, for
// dashboards imported with a custom icon, a legacy base64 data URI.
image: string;
onChange: (value: string) => void;
// Consumers set the trigger's border-radius (e.g. rounded-left when joined to
@@ -21,28 +23,15 @@ interface Props {
}
// Icon picker shared by the dashboard-details settings and the create-dashboard
// modal so both choose from the same `Base64Icons` set.
// modal so both choose from the same system icon set. A custom/legacy value is
// kept as the first option so it stays selected and round-trips.
function DashboardImagePicker({
image,
onChange,
triggerClassName,
}: Props): JSX.Element {
// A custom image (pasted URL / base64 data-URI, not in the preset set) is kept as
// a selectable option for the picker's lifetime — without a matching option the
// trigger renders the raw value string and the image can't be re-selected.
const [customImages, setCustomImages] = useState<string[]>(() =>
image && !Base64Icons.includes(image) ? [image] : [],
);
useEffect(() => {
if (image && !Base64Icons.includes(image)) {
setCustomImages((prev) => (prev.includes(image) ? prev : [...prev, image]));
}
}, [image]);
const options = useMemo(
() => [...customImages, ...Base64Icons],
[customImages],
);
const isCustom = !!image && !SYSTEM_ICON_PATHS.includes(image);
const options = isCustom ? [image, ...SYSTEM_ICON_PATHS] : SYSTEM_ICON_PATHS;
return (
<Select value={image} onChange={(value): void => onChange(value as string)}>
@@ -50,7 +39,11 @@ function DashboardImagePicker({
<SelectContent className={styles.options} withPortal={false}>
{options.map((icon) => (
<SelectItem key={icon} value={icon} className={styles.item}>
<img src={icon} alt="dashboard-icon" className={styles.image} />
<img
src={resolveDashboardImage(icon)}
alt="dashboard-icon"
className={styles.image}
/>
</SelectItem>
))}
</SelectContent>

View File

@@ -15,7 +15,8 @@ import { useOptimisticPatch } from '../../hooks/useOptimisticPatch';
import CrossPanelSync from './CrossPanelSync/CrossPanelSync';
import DashboardInfoForm from './DashboardInfoForm/DashboardInfoForm';
import UnsavedChangesFooter from './UnsavedChangesFooter/UnsavedChangesFooter';
import { Base64Icons, stringsToTags, tagsToStrings } from './utils';
import { stringsToTags, tagsToStrings } from './utils';
import { DEFAULT_DASHBOARD_ICON_PATH } from 'pages/DashboardPageV2/DashboardContainer/dashboardIcons';
import styles from './Overview.module.scss';
interface OverviewProps {
@@ -29,7 +30,7 @@ function Overview({ dashboard }: OverviewProps): JSX.Element {
const title = dashboard.spec.display.name;
const description = dashboard.spec.display.description ?? '';
const image = dashboard.image || Base64Icons[0];
const image = dashboard.image || DEFAULT_DASHBOARD_ICON_PATH;
const tagsAsStrings = useMemo(
() => tagsToStrings(dashboard.tags ?? []),
[dashboard.tags],
@@ -87,7 +88,7 @@ function Overview({ dashboard }: OverviewProps): JSX.Element {
if (updatedImage !== image) {
// `replace` fails when the image doesn't exist yet, so add it when the
// dashboard has none (`add` creates or replaces the member). Key off the
// raw stored value, not the `Base64Icons[0]`-defaulted local `image`.
// raw stored value, not the default-filled local `image`.
ops.push(
op(
dashboard.image

View File

@@ -1,6 +1,5 @@
import type { TagtypesPostableTagDTO } from 'api/generated/services/sigNoz.schemas';
export { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
export { parseKeyValueTag } from 'components/TagKeyValueInput/utils';
// The tag editor is strictly key:value, so always render both sides — a

View File

@@ -120,7 +120,7 @@ export const SECTION_REGISTRY: {
[SectionKind.ContextLinks]: {
Component: ContextLinksSection,
// Panel-level slice (spec.links), not under the plugin spec — no cast needed.
get: (spec): DashboardtypesLinkDTO[] => spec.links,
get: (spec): DashboardtypesLinkDTO[] => spec.links || [],
update: (spec, links): PanelSpec => ({ ...spec, links }),
},
// One editor for every threshold variant (label / comparison / table); the kind's

View File

@@ -1,4 +1,4 @@
import type { DashboardtypesLinkDTO } from 'api/generated/services/sigNoz.schemas';
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { resolveTexts } from 'hooks/dashboard/useContextVariables';
import { resolveContextLinkUrl } from './resolveContextLinkUrl';
@@ -15,7 +15,7 @@ export interface ResolvedDrilldownLink {
* label + URL, drops links without a URL, and skips substitution when `renderVariables === false`.
*/
export function resolvePanelContextLinks(
links: DashboardtypesLinkDTO[] | undefined,
links: DashboardtypesPanelSpecDTO['links'],
processedVariables: Record<string, string>,
): ResolvedDrilldownLink[] {
const usable = (links ?? []).filter((link) => !!link.url);

View File

@@ -8,7 +8,7 @@ import {
ScrollText,
} from '@signozhq/icons';
import logEvent from 'api/common/logEvent';
import type { DashboardtypesLinkDTO } from 'api/generated/services/sigNoz.schemas';
import type { DashboardtypesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { getAggregateColumnHeader } from 'container/QueryTable/Drilldown/drilldownUtils';
import ContextMenu from 'periscope/components/ContextMenu';
import type { DrilldownContext } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/drilldown';
@@ -29,7 +29,7 @@ interface DrilldownAggregateMenuProps {
/** While dashboard variables resolve, the actions show a spinner and are disabled. */
isResolving?: boolean;
/** Panel's context links; resolved against the clicked point + variables here. */
links: DashboardtypesLinkDTO[] | undefined;
links: DashboardtypesPanelSpecDTO['links'];
/** Whether the clicked point exposes group-by fields to bind to dashboard variables. */
canSetDashboardVariables: boolean;
onViewLogs: () => void;

View File

@@ -1,3 +1,11 @@
// Clamp the dashboard icon so a custom base64 SVG (which may carry no intrinsic
// size) can't balloon past the breadcrumb row.
.icon {
width: 16px;
height: 16px;
object-fit: contain;
}
// Cap the dashboard-name segment so a long title ellipsizes on one line instead
// of stretching the breadcrumb row.
.title {

View File

@@ -61,7 +61,9 @@ function DashboardPageBreadcrumbs({
</BreadcrumbItem>
<BreadcrumbSeparator>/</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink icon={<img src={image} alt="dashboard-icon" />}>
<BreadcrumbLink
icon={<img src={image} alt="dashboard-icon" className={styles.icon} />}
>
<span className={styles.title} title={title}>
{title}
</span>

View File

@@ -0,0 +1,79 @@
import {
DEFAULT_DASHBOARD_ICON_PATH,
isValidDashboardImage,
resolveDashboardImage,
SYSTEM_ICON_PATHS,
} from './dashboardIcons';
// The glob-backed icon URL map is Vite-only; jest redirects it to a stub via
// moduleNameMapper (see jest.config.ts), so path lookups return a stable value
// and the resolver's fallback is deterministic under test.
describe('resolveDashboardImage', () => {
const fallback = resolveDashboardImage(undefined);
it('passes through valid base64 image data URIs unchanged', () => {
const svg = 'data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=';
const png = 'data:image/png;base64,iVBORw0KGgo=';
expect(resolveDashboardImage(svg)).toBe(svg);
expect(resolveDashboardImage(png)).toBe(png);
});
it.each([
['non-image data URI', 'data:text/html;base64,PHNjcmlwdD4='],
['javascript URI', 'javascript:alert(1)'],
['remote URL', 'http://evil.com/x.svg'],
['raw markup', '<svg onload=alert(1)>'],
['empty string', ''],
['unknown icon path', '/assets/Icons/does-not-exist'],
['unknown logo path', '/assets/Logos/does-not-exist'],
])('falls back for %s', (_label, value) => {
const resolved = resolveDashboardImage(value);
expect(resolved).not.toBe(value);
expect(resolved).toBe(fallback);
});
it('resolves a known system icon path to a truthy src', () => {
expect(resolveDashboardImage('/assets/Icons/eight-ball')).toBeTruthy();
});
it('resolves a known logo path to a truthy src', () => {
expect(resolveDashboardImage('/assets/Logos/aws-dark')).toBe('mock-logo-url');
});
});
describe('system icon catalogue', () => {
it('exposes 20 icons, all under the /assets/Icons/ prefix', () => {
expect(SYSTEM_ICON_PATHS).toHaveLength(20);
expect(SYSTEM_ICON_PATHS.every((p) => p.startsWith('/assets/Icons/'))).toBe(
true,
);
});
it('defaults to the eight-ball icon', () => {
expect(DEFAULT_DASHBOARD_ICON_PATH).toBe('/assets/Icons/eight-ball');
expect(SYSTEM_ICON_PATHS).toContain(DEFAULT_DASHBOARD_ICON_PATH);
});
});
describe('isValidDashboardImage', () => {
it.each([
['icon path', '/assets/Icons/eight-ball'],
['unknown-but-well-formed icon path', '/assets/Icons/whatever'],
['logo path', '/assets/Logos/aws-dark'],
['base64 image', 'data:image/svg+xml;base64,PHN2Zz48L3N2Zz4='],
['empty (no image)', ''],
])('accepts %s', (_label, value) => {
expect(isValidDashboardImage(value)).toBe(true);
});
it.each([
['remote URL', 'http://evil.com/x.svg'],
['javascript URI', 'javascript:alert(1)'],
['non-image data URI', 'data:text/html;base64,PHNjcmlwdD4='],
['raw markup', '<svg onload=alert(1)>'],
['prefix with no name', '/assets/Icons/'],
['plain string', 'not-a-path'],
])('rejects %s', (_label, value) => {
expect(isValidDashboardImage(value)).toBe(false);
});
});

View File

@@ -0,0 +1,89 @@
import { ICON_URLS, LOGO_URLS } from './iconAssets';
/** Prefix that marks a dashboard `image` as a reference to a bundled system icon. */
export const DASHBOARD_ICON_PATH_PREFIX = '/assets/Icons/';
/** Prefix for a bundled logo (JSON-only; logos are never shown in the picker). */
export const DASHBOARD_LOGO_PATH_PREFIX = '/assets/Logos/';
// Each path prefix maps a reference to the asset map it resolves against.
const PATH_LOOKUPS: { prefix: string; urls: Record<string, string> }[] = [
{ prefix: DASHBOARD_ICON_PATH_PREFIX, urls: ICON_URLS },
{ prefix: DASHBOARD_LOGO_PATH_PREFIX, urls: LOGO_URLS },
];
// The curated icons surfaced in the picker. Any other on-disk icon (e.g.
// `/assets/Icons/broom`) still resolves — this list is only what we show.
const SYSTEM_ICON_NAMES = [
'eight-ball',
'siren',
'avocado',
'bagel',
'basketball',
'carousel-horse',
'cookie',
'circus-tent',
'crane',
'dartboard',
'drum',
'bow',
'cheese',
'orange',
'ninja',
'fries',
'hamburger',
'headphones',
'motorcycle',
'police-car',
];
/** Logical icon paths for the picker, in display order. */
export const SYSTEM_ICON_PATHS: string[] = SYSTEM_ICON_NAMES.map(
(name) => `${DASHBOARD_ICON_PATH_PREFIX}${name}`,
);
/** Fallback used when a dashboard's `image` is empty or unrecognized. */
export const DEFAULT_DASHBOARD_ICON_PATH = `${DASHBOARD_ICON_PATH_PREFIX}eight-ball`;
// A base64 image data URI is the only free-form value we render, and only via
// <img> (which never executes embedded SVG scripts). Everything else is rejected.
const BASE64_IMAGE_REGEX =
/^data:image\/(?:png|jpe?g|gif|webp|avif|svg\+xml);base64,[a-z0-9+/]+={0,2}$/i;
/**
* Resolves a dashboard `image` value to a usable <img> src. Only three shapes are
* allowed — a system icon path (`/assets/Icons/<name>`), a logo path
* (`/assets/Logos/<name>`), each resolving to any matching asset in that folder,
* or a base64 image data URI — so arbitrary URLs or markup can never reach the
* DOM. Anything else (and unknown asset names) falls back to the default icon.
*/
export function resolveDashboardImage(image?: string | null): string {
const fallback = ICON_URLS['eight-ball'] ?? DEFAULT_DASHBOARD_ICON_PATH;
if (!image) {
return fallback;
}
const lookup = PATH_LOOKUPS.find(({ prefix }) => image.startsWith(prefix));
if (lookup) {
return lookup.urls[image.slice(lookup.prefix.length)] ?? fallback;
}
if (BASE64_IMAGE_REGEX.test(image)) {
return image;
}
return fallback;
}
/**
* Whether an `image` value is acceptable to store: a system icon path, a logo
* path, or a base64 image data URI. Empty means "no image" (uses the default)
* and is allowed. Rejects URLs, markup, and other free-form strings. Shape-only
* — a well-formed but unknown path still resolves to the fallback at render.
*/
export function isValidDashboardImage(image?: string | null): boolean {
if (!image) {
return true;
}
const hasKnownPrefix = PATH_LOOKUPS.some(
({ prefix }) => image.startsWith(prefix) && image.length > prefix.length,
);
return hasKnownPrefix || BASE64_IMAGE_REGEX.test(image);
}

View File

@@ -0,0 +1,39 @@
// Maps every SVG/PNG under `src/assets/Icons` and `src/assets/Logos` by file name
// to its bundled reference, so any `/assets/Icons/<name>` or `/assets/Logos/<name>`
// in dashboard JSON resolves without a hand-maintained list. `import.meta.glob` is
// Vite-only, so it lives in this tiny module that jest mocks (see the test).
//
// Icons are the always-present set (shown in the picker), so we let Vite inline the
// small ones as data URIs — they ship in the bundle and render with no network
// call. Logos are a large, JSON-only catalogue, so `?url` keeps them as emitted
// files fetched (and cached) on demand rather than bloating the bundle.
const iconModules = import.meta.glob('../../../assets/Icons/**/*.svg', {
eager: true,
import: 'default',
});
const logoModules = import.meta.glob('../../../assets/Logos/**/*.{svg,png}', {
eager: true,
query: '?url',
import: 'default',
});
/** Maps each asset's file name (without extension) to its bundled reference. */
function toNameMap(modules: Record<string, unknown>): Record<string, string> {
return Object.entries(modules).reduce<Record<string, string>>(
(acc, [path, ref]) => {
const name = path
.split('/')
.pop()
?.replace(/\.(svg|png)$/, '');
if (name) {
acc[name] = ref as string;
}
return acc;
},
{},
);
}
export const ICON_URLS = toNameMap(iconModules);
export const LOGO_URLS = toNameMap(logoModules);

View File

@@ -14,7 +14,7 @@ import DashboardPageHeader from './components/DashboardPageHeader/DashboardPageH
import LockedIndicator from './components/LockedIndicator/LockedIndicator';
import DashboardChangedDialog from './components/DashboardChangedDialog/DashboardChangedDialog';
import { useDashboardStaleCheck } from './hooks/useDashboardStaleCheck';
import { Base64Icons } from './DashboardSettings/Overview/utils';
import { resolveDashboardImage } from 'pages/DashboardPageV2/DashboardContainer/dashboardIcons';
interface DashboardContainerProps {
dashboard: DashboardtypesGettableDashboardV2DTO;
@@ -26,7 +26,7 @@ function DashboardContainer({
refetch,
}: DashboardContainerProps): JSX.Element {
const spec = dashboard.spec;
const image = dashboard.image || Base64Icons[0];
const image = resolveDashboardImage(dashboard.image);
const name = spec.display.name;
useEffect(() => {

View File

@@ -49,7 +49,6 @@ export function createDefaultPanel(
spec: pluginSpec,
} as DashboardtypesPanelPluginDTO,
queries,
links: [],
},
};
}

View File

@@ -7,7 +7,7 @@ import { CalendarClock, LockKeyhole, Pin, PinOff } from '@signozhq/icons';
import cx from 'classnames';
import logEvent from 'api/common/logEvent';
import { generatePath } from 'react-router-dom';
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
import { resolveDashboardImage } from 'pages/DashboardPageV2/DashboardContainer/dashboardIcons';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
@@ -54,7 +54,7 @@ function DashboardRow({
const isPinned = !!dashboard.pinned;
const id = dashboard.id;
const name = dashboard.spec?.display?.name ?? '';
const image = dashboard.image || Base64Icons[0];
const image = resolveDashboardImage(dashboard.image);
const createdBy = dashboard.createdBy ?? '';
const updatedBy = dashboard.updatedBy ?? '';
const createdAt = dashboard.createdAt ?? '';

View File

@@ -19,7 +19,7 @@ import TagKeyValueInput from 'components/TagKeyValueInput/TagKeyValueInput';
import { keyValueStringsToTags } from '../../utils/helpers';
import DashboardImagePicker from '../../../DashboardPageV2/DashboardContainer/DashboardSettings/Overview/DashboardImagePicker/DashboardImagePicker';
import { Base64Icons } from '../../../DashboardPageV2/DashboardContainer/DashboardSettings/Overview/utils';
import { DEFAULT_DASHBOARD_ICON_PATH } from 'pages/DashboardPageV2/DashboardContainer/dashboardIcons';
import { DASHBOARD_NAME_MAX_LENGTH } from '../../../DashboardPageV2/DashboardContainer/constants';
import styles from './NewDashboardModal.module.scss';
@@ -35,7 +35,7 @@ function BlankDashboardPanel({ onClose }: Props): JSX.Element {
const [name, setName] = useState(DEFAULT_NAME);
const [description, setDescription] = useState('');
const [image, setImage] = useState<string>(Base64Icons[0]);
const [image, setImage] = useState<string>(DEFAULT_DASHBOARD_ICON_PATH);
const [tags, setTags] = useState<string[]>([]);
const [submitting, setSubmitting] = useState(false);
@@ -62,7 +62,6 @@ function BlankDashboardPanel({ onClose }: Props): JSX.Element {
layouts: [],
panels: {},
variables: [],
links: [],
},
});
void logEvent(DashboardListEvents.DashboardCreated, {

View File

@@ -16,6 +16,8 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
import { DashboardListEvents } from 'pages/DashboardsListPageV2/constants/events';
import APIError from 'types/api/error';
import { isValidDashboardImage } from 'pages/DashboardPageV2/DashboardContainer/dashboardIcons';
import { normalizeToPostable } from './importUtils';
import JsonEditor from './JsonEditor';
@@ -64,6 +66,18 @@ function ImportJsonPanel({ onClose }: Props): JSX.Element {
logEvent('Dashboard List V2: Import and next clicked', {});
const parsed = JSON.parse(editorValue) as Record<string, unknown>;
const payload = normalizeToPostable(parsed);
// Only an icon/logo path or a base64 image is a storable `image`; reject
// URLs/markup/other rather than import a value that won't render.
if (!isValidDashboardImage(payload.image)) {
setIsCreateError(true);
void logEvent(DashboardListEvents.ImportFailed, {
reason: 'invalid-image',
});
toast.error(
'Dashboard "image" must be an /assets/Icons or /assets/Logos path, or a base64 image',
);
return;
}
const response = await createDashboardV2(payload);
void logEvent(DashboardListEvents.DashboardCreated, { method: 'import' });
onClose();

View File

@@ -241,9 +241,12 @@ func (server *Server) PutAlerts(ctx context.Context, postableAlerts alertmanager
}
func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertmanagertypes.Config) error {
config := alertmanagerConfig.AlertmanagerConfig()
resolved, err := alertmanagerConfig.Resolved()
if err != nil {
return err
}
config := resolved.AlertmanagerConfig()
var err error
// Load SigNoz's alertmanager notification templates from the configured
// globs. The upstream default templates (default.tmpl, email.tmpl) are
// always loaded from the embedded alertmanager assets inside FromGlobs, so
@@ -275,7 +278,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
server.logger.InfoContext(ctx, "skipping creation of receiver not referenced by any route", slog.String("receiver", rcv.Name))
continue
}
extendedRcv, err := alertmanagerConfig.GetReceiver(rcv.Name)
extendedRcv, err := resolved.GetReceiver(rcv.Name)
if err != nil {
return err
}
@@ -350,7 +353,7 @@ func (server *Server) SetConfig(ctx context.Context, alertmanagerConfig *alertma
go server.dispatcher.Run()
go server.inhibitor.Run()
server.alertmanagerConfig = alertmanagerConfig
server.alertmanagerConfig = resolved
return nil
}

View File

@@ -167,6 +167,10 @@ func (provider *provider) UpdateChannelByReceiverAndID(ctx context.Context, orgI
return err
}
if err := config.SetGlobalConfig(provider.config.Signoz.Global); err != nil {
return err
}
if err := config.UpdateReceiver(receiver); err != nil {
return err
}
@@ -217,6 +221,10 @@ func (provider *provider) CreateChannel(ctx context.Context, orgID string, recei
return nil, err
}
if err := config.SetGlobalConfig(provider.config.Signoz.Global); err != nil {
return nil, err
}
if err := config.CreateReceiver(receiver); err != nil {
return nil, err
}

View File

@@ -0,0 +1,162 @@
package signozalertmanager
import (
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagerserver"
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlitesqlstore"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/prometheus/alertmanager/config"
)
func newTestSQLStore(t *testing.T) sqlstore.SQLStore {
t.Helper()
store, err := sqlitesqlstore.New(t.Context(), factorytest.NewSettings(), sqlstore.Config{
Provider: "sqlite",
Connection: sqlstore.ConnectionConfig{
MaxOpenConns: 1,
MaxConnLifetime: 0,
},
Sqlite: sqlstore.SqliteConfig{
Path: filepath.Join(t.TempDir(), "test.db"),
Mode: "wal",
BusyTimeout: 5 * time.Second,
TransactionMode: "deferred",
},
})
require.NoError(t, err)
_, err = store.BunDB().NewCreateTable().
Model((*alertmanagertypes.StoreableConfig)(nil)).
IfNotExists().
Exec(t.Context())
require.NoError(t, err)
_, err = store.BunDB().ExecContext(t.Context(), "CREATE UNIQUE INDEX IF NOT EXISTS idx_alertmanager_config_org_id ON alertmanager_config(org_id)")
require.NoError(t, err)
_, err = store.BunDB().NewCreateTable().
Model((*alertmanagertypes.Channel)(nil)).
IfNotExists().
Exec(t.Context())
require.NoError(t, err)
return store
}
func newTestProvider(t *testing.T) (*provider, sqlstore.SQLStore) {
t.Helper()
serverConfig := alertmanagerserver.NewConfig()
serverConfig.Global.SMTPFrom = "alerts@example.com"
serverConfig.Global.SMTPSmarthost = config.HostPort{Host: "smtp.sendgrid.net", Port: "587"}
serverConfig.Global.SMTPAuthUsername = "apikey"
serverConfig.Global.SMTPAuthPassword = "operator-secret"
sqlStore := newTestSQLStore(t)
p, err := New(
factorytest.NewSettings(),
alertmanager.Config{
Provider: "signoz",
Signoz: alertmanager.Signoz{
PollInterval: time.Minute,
Config: serverConfig,
},
},
sqlStore,
nil,
nil,
nil,
)
require.NoError(t, err)
return p, sqlStore
}
func requireNoSMTPSettings(t *testing.T, payload string) {
t.Helper()
assert.NotContains(t, payload, "operator-secret")
assert.NotContains(t, payload, "smtp.sendgrid.net")
assert.NotContains(t, payload, "apikey")
assert.NotContains(t, payload, "auth_password")
assert.NotContains(t, payload, "smtp_auth_password")
}
func TestCreateEmailChannelWithStoredConfigWithoutSMTP(t *testing.T) {
p, _ := newTestProvider(t)
orgID := "test-org-1"
require.NoError(t, p.SetDefaultConfig(t.Context(), orgID))
seeded, err := p.GetConfig(t.Context(), orgID)
require.NoError(t, err)
requireNoSMTPSettings(t, seeded.StoreableConfig().Config)
receiver, err := alertmanagertypes.NewReceiver(`{"name":"email-receiver","email_configs":[{"to":"team@example.com"}]}`)
require.NoError(t, err)
channel, err := p.CreateChannel(t.Context(), orgID, receiver)
require.NoError(t, err)
requireNoSMTPSettings(t, channel.Data)
stored, err := p.GetChannelByID(t.Context(), orgID, channel.ID)
require.NoError(t, err)
assert.Contains(t, stored.Data, "team@example.com")
requireNoSMTPSettings(t, stored.Data)
cfg, err := p.GetConfig(t.Context(), orgID)
require.NoError(t, err)
requireNoSMTPSettings(t, cfg.StoreableConfig().Config)
require.NoError(t, cfg.SetGlobalConfig(p.config.Signoz.Global))
resolved, err := cfg.Resolved()
require.NoError(t, err)
resolvedReceiver, err := resolved.GetReceiver("email-receiver")
require.NoError(t, err)
require.Len(t, resolvedReceiver.EmailConfigs, 1)
assert.Equal(t, "smtp.sendgrid.net:587", resolvedReceiver.EmailConfigs[0].Smarthost.String())
assert.Equal(t, "operator-secret", string(resolvedReceiver.EmailConfigs[0].AuthPassword))
}
func TestUpdateStampedLegacyEmailChannel(t *testing.T) {
p, sqlStore := newTestProvider(t)
orgID := "test-org-2"
require.NoError(t, p.SetDefaultConfig(t.Context(), orgID))
receiver, err := alertmanagertypes.NewReceiver(`{"name":"email-receiver","email_configs":[{"to":"team@example.com"}]}`)
require.NoError(t, err)
channel, err := p.CreateChannel(t.Context(), orgID, receiver)
require.NoError(t, err)
stampedData := `{"name":"email-receiver","email_configs":[{"send_resolved":false,"to":"team@example.com","from":"old@example.com","hello":"localhost","smarthost":"email-smtp.us-east-1.amazonaws.com:587","auth_username":"AKIA000","auth_password":"old-ses-secret","require_tls":true}]}`
_, err = sqlStore.BunDB().ExecContext(t.Context(), "UPDATE notification_channel SET data = ? WHERE id = ?", stampedData, channel.ID.StringValue())
require.NoError(t, err)
updated, err := alertmanagertypes.NewReceiver(`{"name":"email-receiver","email_configs":[{"to":"new-team@example.com"}]}`)
require.NoError(t, err)
require.NoError(t, p.UpdateChannelByReceiverAndID(t.Context(), orgID, updated, channel.ID))
stored, err := p.GetChannelByID(t.Context(), orgID, channel.ID)
require.NoError(t, err)
assert.Contains(t, stored.Data, "new-team@example.com")
assert.NotContains(t, stored.Data, "old-ses-secret")
assert.NotContains(t, stored.Data, "amazonaws.com")
requireNoSMTPSettings(t, stored.Data)
cfg, err := p.GetConfig(t.Context(), orgID)
require.NoError(t, err)
assert.NotContains(t, cfg.StoreableConfig().Config, "old-ses-secret")
requireNoSMTPSettings(t, cfg.StoreableConfig().Config)
}

View File

@@ -351,6 +351,23 @@ func (store *store) Update(ctx context.Context, orgID valuer.UUID, storableDashb
return nil
}
func (store *store) UpdateName(ctx context.Context, orgID valuer.UUID, id valuer.UUID, name string) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewUpdate().
Model((*dashboardtypes.StorableDashboard)(nil)).
Set("name = ?", name).
Where("id = ?", id).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", id)
}
return nil
}
func (store *store) UpdatePublic(ctx context.Context, storable *dashboardtypes.StorablePublicDashboard) error {
_, err := store.
sqlstore.

View File

@@ -1,16 +1,21 @@
package impldashboard
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/transition"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/tagtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -27,12 +32,39 @@ func (handler *handler) CreateV2(rw http.ResponseWriter, r *http.Request) {
orgID := valuer.MustNewUUID(claims.OrgID)
var req dashboardtypes.PostableDashboardV2
if err := binding.JSON.BindBody(r.Body, &req); err != nil {
// Read the body ourselves (rather than binding straight from r.Body) so the
// raw bytes survive a failed bind for the v1 fallback below.
body, err := io.ReadAll(r.Body)
if err != nil {
render.Error(rw, err)
return
}
var req dashboardtypes.PostableDashboardV2
if err := binding.JSON.BindBody(bytes.NewReader(body), &req); err != nil {
// Fallback: the body may be a legacy v1 dashboard. Migrate it to v2 (same
// v4→v5 + v1→v2 pass the bulk migration runs) and retry; if it isn't a
// convertible v1 payload either, surface the original v2 binding error.
var data map[string]any
if json.Unmarshal(body, &data) != nil {
render.Error(rw, err)
return
}
storable := dashboardtypes.StorableDashboard{Data: data, OrgID: orgID}
transition.NewDashboardMigrateV5(handler.providerSettings.Logger, nil, nil).Migrate(ctx, storable.Data)
v2, convErr := storable.ConvertV1ToV2()
if convErr != nil {
render.Error(rw, err)
return
}
req = dashboardtypes.PostableDashboardV2{
DashboardV2MetadataBase: v2.DashboardV2MetadataBase,
Name: v2.Name,
Tags: tagtypes.NewPostableTagsFromTags(v2.Tags),
Spec: v2.Spec,
}
}
dashboard, err := handler.module.CreateV2(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.IdentityID()), dashboardtypes.SourceUser, req)
if err != nil {
render.Error(rw, err)

View File

@@ -88,13 +88,9 @@ func (m *module) getTopClusterGroups(
orgID valuer.UUID,
req *inframonitoringtypes.PostableClusters,
metadataMap map[string]map[string]string,
podStatusCountsMap map[string]podStatusCounts,
nodeConditionCountsMap map[string]nodeConditionCounts,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.ClusterNameAttrKey {
// metadataMap is already status-filtered by the caller, so the name
// branch needs no extra intersection.
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.ClusterNameAttrKey), nil
}
queryNamesForOrderBy := orderByToClustersQueryNames[orderByKey]
@@ -136,17 +132,6 @@ func (m *module) getTopClusterGroups(
}
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
// When filtering by a secondary status, intersect the ranked groups against
// its keyset — an empty map (no match) must yield an empty page, not the full
// set. Both filters compose as AND.
if req.Filter != nil {
if !req.Filter.FilterByPodStatus.IsZero() {
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
}
if !req.Filter.FilterByNodeReadiness.IsZero() {
allMetricGroups = intersectRankedGroups(allMetricGroups, nodeConditionCountsMap)
}
}
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
}
@@ -157,9 +142,5 @@ func (m *module) getClustersTableMetadata(ctx context.Context, orgID valuer.UUID
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, clustersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, clustersTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -94,12 +94,9 @@ func (m *module) getTopDaemonSetGroups(
orgID valuer.UUID,
req *inframonitoringtypes.PostableDaemonSets,
metadataMap map[string]map[string]string,
podStatusCountsMap map[string]podStatusCounts,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.DaemonSetNameAttrKey {
// metadataMap is already status-filtered by the caller, so the name
// branch needs no extra intersection.
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DaemonSetNameAttrKey), nil
}
queryNamesForOrderBy := orderByToDaemonSetsQueryNames[orderByKey]
@@ -141,11 +138,6 @@ func (m *module) getTopDaemonSetGroups(
}
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
// When filtering by status, intersect the ranked groups against the keyset —
// an empty map (no group matched) must yield an empty page, not the full set.
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() {
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
}
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
}
@@ -156,9 +148,5 @@ func (m *module) getDaemonSetsTableMetadata(ctx context.Context, orgID valuer.UU
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, daemonSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, daemonSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -86,12 +86,9 @@ func (m *module) getTopDeploymentGroups(
orgID valuer.UUID,
req *inframonitoringtypes.PostableDeployments,
metadataMap map[string]map[string]string,
podStatusCountsMap map[string]podStatusCounts,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.DeploymentNameAttrKey {
// metadataMap is already status-filtered by the caller, so the name
// branch needs no extra intersection.
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.DeploymentNameAttrKey), nil
}
queryNamesForOrderBy := orderByToDeploymentsQueryNames[orderByKey]
@@ -133,11 +130,6 @@ func (m *module) getTopDeploymentGroups(
}
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
// When filtering by status, intersect the ranked groups against the keyset —
// an empty map (no group matched) must yield an empty page, not the full set.
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() {
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
}
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
}
@@ -148,9 +140,5 @@ func (m *module) getDeploymentsTableMetadata(ctx context.Context, orgID valuer.U
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, deploymentsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, deploymentsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -63,33 +63,6 @@ func compositeKeyFromLabels(labels map[string]string, groupBy []qbtypes.GroupByK
return compositeKeyFromList(parts)
}
// intersectMap returns the entries of m whose key is present in keep (a new
// map). keep's value type is irrelevant — only its keys are read — so a
// per-group counts map (already filtered by the SQL push-down) can be passed
// directly. Used to trim metadataMap to the status-matching groups.
func intersectMap[V any, K any](m map[string]V, keep map[string]K) map[string]V {
out := make(map[string]V, len(m))
for k, v := range m {
if _, ok := keep[k]; ok {
out[k] = v
}
}
return out
}
// intersectRankedGroups returns the ranked groups whose compositeKey is present
// in keep, preserving order. Keeps status-unmatched groups out of the ranked
// page slots.
func intersectRankedGroups[K any](groups []rankedGroup, keep map[string]K) []rankedGroup {
out := make([]rankedGroup, 0, len(groups))
for _, g := range groups {
if _, ok := keep[g.compositeKey]; ok {
out = append(out, g)
}
}
return out
}
// parseAndSortGroups extracts group label maps from a ScalarData response and
// sorts them by the ranking query's aggregation value.
func parseAndSortGroups(

View File

@@ -1,7 +1,6 @@
package implinframonitoring
import (
"reflect"
"testing"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
@@ -384,81 +383,3 @@ func TestCompositeKeyFromLabels(t *testing.T) {
})
}
}
func TestIntersectMap(t *testing.T) {
tests := []struct {
name string
m map[string]int
keep map[string]podStatusCounts
expected map[string]int
}{
{
name: "keep subset",
m: map[string]int{"a": 1, "b": 2, "c": 3},
keep: map[string]podStatusCounts{"a": {}, "c": {}},
expected: map[string]int{"a": 1, "c": 3},
},
{
name: "empty keep drops everything",
m: map[string]int{"a": 1, "b": 2},
keep: map[string]podStatusCounts{},
expected: map[string]int{},
},
{
name: "keep key absent from m is ignored",
m: map[string]int{"a": 1},
keep: map[string]podStatusCounts{"a": {}, "z": {}},
expected: map[string]int{"a": 1},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := intersectMap(tt.m, tt.keep)
if !reflect.DeepEqual(got, tt.expected) {
t.Errorf("intersectMap(%v, keys=%v) = %v, want %v", tt.m, tt.keep, got, tt.expected)
}
})
}
}
func TestIntersectRankedGroups(t *testing.T) {
groups := []rankedGroup{
{compositeKey: "a", value: 3},
{compositeKey: "b", value: 2},
{compositeKey: "c", value: 1},
}
tests := []struct {
name string
groups []rankedGroup
keep map[string]podStatusCounts
expected []string // compositeKeys in order
}{
{
name: "preserves order, drops non-matching",
groups: groups,
keep: map[string]podStatusCounts{"a": {}, "c": {}},
expected: []string{"a", "c"},
},
{
name: "empty keep drops all",
groups: groups,
keep: map[string]podStatusCounts{},
expected: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := intersectRankedGroups(tt.groups, tt.keep)
gotKeys := make([]string, 0, len(got))
for _, g := range got {
gotKeys = append(gotKeys, g.compositeKey)
}
if !reflect.DeepEqual(gotKeys, tt.expected) {
t.Errorf("intersectRankedGroups keys = %v, want %v", gotKeys, tt.expected)
}
})
}
}

View File

@@ -94,12 +94,9 @@ func (m *module) getTopJobGroups(
orgID valuer.UUID,
req *inframonitoringtypes.PostableJobs,
metadataMap map[string]map[string]string,
podStatusCountsMap map[string]podStatusCounts,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.JobNameAttrKey {
// metadataMap is already status-filtered by the caller, so the name
// branch needs no extra intersection.
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.JobNameAttrKey), nil
}
queryNamesForOrderBy := orderByToJobsQueryNames[orderByKey]
@@ -141,11 +138,6 @@ func (m *module) getTopJobGroups(
}
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
// When filtering by status, intersect the ranked groups against the keyset —
// an empty map (no group matched) must yield an empty page, not the full set.
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() {
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
}
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
}
@@ -156,9 +148,5 @@ func (m *module) getJobsTableMetadata(ctx context.Context, orgID valuer.UUID, re
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, jobsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, jobsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -291,61 +291,14 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
return resp, nil
}
var (
podFilter *qbtypes.Filter
filterByPodStatus inframonitoringtypes.PodStatus
filterExpr string
metadataMap map[string]map[string]string
queryResp *qbtypes.QueryRangeResponse
statusCounts map[string]podStatusCounts
statusWarning *qbtypes.QueryWarnData
restartCounts map[string]int64
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
podFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
// Metadata, plus a full-scope status resolution when filtering by pod status
// (pageGroups=nil spans all groups under the user filter). When not filtering,
// status is computed page-scoped in the fan-out below.
gUp, gUpCtx := errgroup.WithContext(ctx)
gUp.Go(func() error {
var err error
metadataMap, err = m.getPodsTableMetadata(gUpCtx, orgID, req)
return err
})
if !filterByPodStatus.IsZero() {
gUp.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gUpCtx, orgID, req.Start, req.End, podFilter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if err := gUp.Wait(); err != nil {
metadataMap, err := m.getPodsTableMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
if !filterByPodStatus.IsZero() {
// Required metric missing: can't derive status, so surface the warning
// and return empty rather than silently filtering everything out.
if statusWarning != nil {
resp.Warning = statusWarning
resp.Records = []inframonitoringtypes.PodRecord{}
resp.Total = 0
return resp, nil
}
// Secondary filter: keep only groups with a status-matching pod.
metadataMap = intersectMap(metadataMap, statusCounts)
}
resp.Total = len(metadataMap)
// statusCounts is the status keyset (nil when not filtering); getTopPodGroups
// intersects the ranked groups against it before paginating.
pageGroups, err := m.getTopPodGroups(ctx, orgID, req, metadataMap, statusCounts)
pageGroups, err := m.getTopPodGroups(ctx, orgID, req, metadataMap)
if err != nil {
return nil, err
}
@@ -355,8 +308,20 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newPodsTableListQuery())
var (
queryResp *qbtypes.QueryRangeResponse
statusCounts map[string]podStatusCounts
statusWarning *qbtypes.QueryWarnData
restartCounts map[string]int64
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -366,18 +331,14 @@ func (m *module) ListPods(ctx context.Context, orgID valuer.UUID, req *inframoni
})
g.Go(func() error {
var err error
restartCounts, err = m.getPerGroupPodRestartCounts(gCtx, orgID, req.Start, req.End, podFilter, req.GroupBy, pageGroups)
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
restartCounts, err = m.getPerGroupPodRestartCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
// When filtering, statusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if filterByPodStatus.IsZero() {
g.Go(func() error {
var err error
statusCounts, statusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, podFilter, req.GroupBy, pageGroups, inframonitoringtypes.PodStatus{})
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
@@ -532,74 +493,14 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
return resp, nil
}
var (
nodeFilter *qbtypes.Filter
filterByPodStatus inframonitoringtypes.PodStatus
filterByNodeReadiness inframonitoringtypes.NodeCondition
filterExpr string
metadataMap map[string]map[string]string
queryResp *qbtypes.QueryRangeResponse
nodeConditionCounts map[string]nodeConditionCounts
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
nodeFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
}
// Metadata, plus full-scope pod status / node readiness resolutions when
// filtering by them (pageGroups=nil spans all groups under the user filter).
// When not filtering, each is computed page-scoped in the fan-out below.
gUp, gUpCtx := errgroup.WithContext(ctx)
gUp.Go(func() error {
var err error
metadataMap, err = m.getNodesTableMetadata(gUpCtx, orgID, req)
return err
})
if !filterByPodStatus.IsZero() {
gUp.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gUpCtx, orgID, req.Start, req.End, nodeFilter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if !filterByNodeReadiness.IsZero() {
gUp.Go(func() error {
var err error
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gUpCtx, orgID, req.Start, req.End, nodeFilter, req.GroupBy, nil, filterByNodeReadiness)
return err
})
}
if err := gUp.Wait(); err != nil {
metadataMap, err := m.getNodesTableMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
if !filterByPodStatus.IsZero() {
// Required metric missing: can't derive status, so surface the warning
// and return empty rather than silently filtering everything out.
if podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.NodeRecord{}
resp.Total = 0
return resp, nil
}
// Secondary filter: keep only nodes with a status-matching pod.
metadataMap = intersectMap(metadataMap, podStatusCounts)
}
if !filterByNodeReadiness.IsZero() {
// Secondary filter (ANDs with pod status): keep only readiness-matching nodes.
metadataMap = intersectMap(metadataMap, nodeConditionCounts)
}
resp.Total = len(metadataMap)
// podStatusCounts / nodeConditionCounts are the status keysets (nil when not
// filtering); getTopNodeGroups intersects the ranked groups against them.
pageGroups, err := m.getTopNodeGroups(ctx, orgID, req, metadataMap, podStatusCounts, nodeConditionCounts)
pageGroups, err := m.getTopNodeGroups(ctx, orgID, req, metadataMap)
if err != nil {
return nil, err
}
@@ -609,8 +510,20 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newNodesTableListQuery())
var (
queryResp *qbtypes.QueryRangeResponse
nodeConditionCounts map[string]nodeConditionCounts
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -618,24 +531,16 @@ func (m *module) ListNodes(ctx context.Context, orgID valuer.UUID, req *inframon
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
return err
})
// When filtering by readiness, nodeConditionCounts already holds the full-scope
// map (a superset of the page); otherwise compute it page-scoped here.
if filterByNodeReadiness.IsZero() {
g.Go(func() error {
var err error
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, nodeFilter, req.GroupBy, pageGroups, inframonitoringtypes.NodeCondition{})
return err
})
}
// When filtering by pod status, podStatusCounts already holds the full-scope
// map; otherwise compute it page-scoped here.
if filterByPodStatus.IsZero() {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, nodeFilter, req.GroupBy, pageGroups, inframonitoringtypes.PodStatus{})
return err
})
}
g.Go(func() error {
var err error
nodeConditionCounts, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
if err := g.Wait(); err != nil {
return nil, err
@@ -686,61 +591,14 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
return resp, nil
}
var (
namespaceFilter *qbtypes.Filter
filterByPodStatus inframonitoringtypes.PodStatus
filterExpr string
metadataMap map[string]map[string]string
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
resourceCounts map[string]map[string]int64
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
namespaceFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
// Metadata, plus a full-scope pod status resolution when filtering by pod
// status (pageGroups=nil spans all groups under the user filter). When not
// filtering, status is computed page-scoped in the fan-out below.
gUp, gUpCtx := errgroup.WithContext(ctx)
gUp.Go(func() error {
var err error
metadataMap, err = m.getNamespacesTableMetadata(gUpCtx, orgID, req)
return err
})
if !filterByPodStatus.IsZero() {
gUp.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gUpCtx, orgID, req.Start, req.End, namespaceFilter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if err := gUp.Wait(); err != nil {
metadataMap, err := m.getNamespacesTableMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
if !filterByPodStatus.IsZero() {
// Required metric missing: can't derive status, so surface the warning
// and return empty rather than silently filtering everything out.
if podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.NamespaceRecord{}
resp.Total = 0
return resp, nil
}
// Secondary filter: keep only namespaces with a status-matching pod.
metadataMap = intersectMap(metadataMap, podStatusCounts)
}
resp.Total = len(metadataMap)
// podStatusCounts is the status keyset (nil when not filtering); getTopNamespaceGroups
// intersects the ranked groups against it before paginating.
pageGroups, err := m.getTopNamespaceGroups(ctx, orgID, req, metadataMap, podStatusCounts)
pageGroups, err := m.getTopNamespaceGroups(ctx, orgID, req, metadataMap)
if err != nil {
return nil, err
}
@@ -750,8 +608,20 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newNamespacesTableListQuery())
var (
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
resourceCounts map[string]map[string]int64
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -761,18 +631,14 @@ func (m *module) ListNamespaces(ctx context.Context, orgID valuer.UUID, req *inf
})
g.Go(func() error {
var err error
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, namespaceFilter, req.GroupBy, pageGroups, namespaceCountAttrKeys, namespacesMetricNamesListForCounts)
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups, namespaceCountAttrKeys, namespacesMetricNamesListForCounts)
return err
})
// When filtering, podStatusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if filterByPodStatus.IsZero() {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, namespaceFilter, req.GroupBy, pageGroups, inframonitoringtypes.PodStatus{})
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
@@ -822,78 +688,14 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
return resp, nil
}
// With default groupBy [k8s.cluster.name], counts are bucketed per cluster;
// with a custom groupBy, they aggregate across clusters in that group.
var (
clusterFilter *qbtypes.Filter
filterByPodStatus inframonitoringtypes.PodStatus
filterByNodeReadiness inframonitoringtypes.NodeCondition
filterExpr string
metadataMap map[string]map[string]string
queryResp *qbtypes.QueryRangeResponse
nodeConditionCountsMap map[string]nodeConditionCounts
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
resourceCounts map[string]map[string]int64
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
clusterFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
filterByNodeReadiness = req.Filter.FilterByNodeReadiness
}
// Metadata, plus full-scope pod status / node readiness resolutions when
// filtering by them (pageGroups=nil spans all groups under the user filter).
// When not filtering, each is computed page-scoped in the fan-out below.
gUp, gUpCtx := errgroup.WithContext(ctx)
gUp.Go(func() error {
var err error
metadataMap, err = m.getClustersTableMetadata(gUpCtx, orgID, req)
return err
})
if !filterByPodStatus.IsZero() {
gUp.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gUpCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if !filterByNodeReadiness.IsZero() {
gUp.Go(func() error {
var err error
nodeConditionCountsMap, err = m.getPerGroupNodeConditionCounts(gUpCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, nil, filterByNodeReadiness)
return err
})
}
if err := gUp.Wait(); err != nil {
metadataMap, err := m.getClustersTableMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
if !filterByPodStatus.IsZero() {
// Required metric missing: can't derive status, so surface the warning
// and return empty rather than silently filtering everything out.
if podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.ClusterRecord{}
resp.Total = 0
return resp, nil
}
// Secondary filter: keep only clusters with a status-matching pod.
metadataMap = intersectMap(metadataMap, podStatusCounts)
}
if !filterByNodeReadiness.IsZero() {
// Secondary filter (ANDs with pod status): keep only clusters with a
// readiness-matching node.
metadataMap = intersectMap(metadataMap, nodeConditionCountsMap)
}
resp.Total = len(metadataMap)
// podStatusCounts / nodeConditionCountsMap are the status keysets (nil when not
// filtering); getTopClusterGroups intersects the ranked groups against them.
pageGroups, err := m.getTopClusterGroups(ctx, orgID, req, metadataMap, podStatusCounts, nodeConditionCountsMap)
pageGroups, err := m.getTopClusterGroups(ctx, orgID, req, metadataMap)
if err != nil {
return nil, err
}
@@ -903,8 +705,23 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newClustersTableListQuery())
// With default groupBy [k8s.cluster.name], counts are bucketed per cluster;
// with a custom groupBy, they aggregate across clusters in that group.
var (
queryResp *qbtypes.QueryRangeResponse
nodeConditionCountsMap map[string]nodeConditionCounts
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
resourceCounts map[string]map[string]int64
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -912,29 +729,21 @@ func (m *module) ListClusters(ctx context.Context, orgID valuer.UUID, req *infra
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
return err
})
// When filtering by readiness, nodeConditionCountsMap already holds the
// full-scope map (a superset of the page); otherwise compute it page-scoped here.
if filterByNodeReadiness.IsZero() {
g.Go(func() error {
var err error
nodeConditionCountsMap, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, pageGroups, inframonitoringtypes.NodeCondition{})
return err
})
}
g.Go(func() error {
var err error
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, pageGroups, clusterCountAttrKeys, clusterMetricNamesListForCounts)
nodeConditionCountsMap, err = m.getPerGroupNodeConditionCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
g.Go(func() error {
var err error
resourceCounts, err = m.getPerGroupDistinctCounts(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups, clusterCountAttrKeys, clusterMetricNamesListForCounts)
return err
})
// When filtering by pod status, podStatusCounts already holds the full-scope
// map; otherwise compute it page-scoped here.
if filterByPodStatus.IsZero() {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, clusterFilter, req.GroupBy, pageGroups, inframonitoringtypes.PodStatus{})
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
@@ -1053,7 +862,7 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
// Bake the deployments base filter into req.Filter so all downstream helpers pick it up.
if req.Filter == nil {
req.Filter = &inframonitoringtypes.DeploymentFilter{}
req.Filter = &qbtypes.Filter{}
}
req.Filter.Expression = mergeFilterExpressions(deploymentsBaseFilterExpr, req.Filter.Expression)
@@ -1068,60 +877,14 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
return resp, nil
}
var (
deploymentFilter *qbtypes.Filter
filterByPodStatus inframonitoringtypes.PodStatus
filterExpr string
metadataMap map[string]map[string]string
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
deploymentFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
// Metadata, plus a full-scope pod status resolution when filtering by pod
// status (pageGroups=nil spans all groups under the user filter). When not
// filtering, status is computed page-scoped in the fan-out below.
gUp, gUpCtx := errgroup.WithContext(ctx)
gUp.Go(func() error {
var err error
metadataMap, err = m.getDeploymentsTableMetadata(gUpCtx, orgID, req)
return err
})
if !filterByPodStatus.IsZero() {
gUp.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gUpCtx, orgID, req.Start, req.End, deploymentFilter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if err := gUp.Wait(); err != nil {
metadataMap, err := m.getDeploymentsTableMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
if !filterByPodStatus.IsZero() {
// Required metric missing: can't derive status, so surface the warning
// and return empty rather than silently filtering everything out.
if podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.DeploymentRecord{}
resp.Total = 0
return resp, nil
}
// Secondary filter: keep only deployments with a status-matching pod.
metadataMap = intersectMap(metadataMap, podStatusCounts)
}
resp.Total = len(metadataMap)
// podStatusCounts is the status keyset (nil when not filtering); getTopDeploymentGroups
// intersects the ranked groups against it before paginating.
pageGroups, err := m.getTopDeploymentGroups(ctx, orgID, req, metadataMap, podStatusCounts)
pageGroups, err := m.getTopDeploymentGroups(ctx, orgID, req, metadataMap)
if err != nil {
return nil, err
}
@@ -1131,8 +894,19 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newDeploymentsTableListQuery())
var (
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -1140,15 +914,11 @@ func (m *module) ListDeployments(ctx context.Context, orgID valuer.UUID, req *in
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
return err
})
// When filtering, podStatusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if filterByPodStatus.IsZero() {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, deploymentFilter, req.GroupBy, pageGroups, inframonitoringtypes.PodStatus{})
return err
})
}
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
if err := g.Wait(); err != nil {
return nil, err
@@ -1189,7 +959,7 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
// Bake the workload base filter into req.Filter so all downstream helpers pick it up.
if req.Filter == nil {
req.Filter = &inframonitoringtypes.StatefulSetFilter{}
req.Filter = &qbtypes.Filter{}
}
req.Filter.Expression = mergeFilterExpressions(statefulSetsBaseFilterExpr, req.Filter.Expression)
@@ -1204,60 +974,14 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
return resp, nil
}
var (
statefulSetFilter *qbtypes.Filter
filterByPodStatus inframonitoringtypes.PodStatus
filterExpr string
metadataMap map[string]map[string]string
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
statefulSetFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
// Metadata, plus a full-scope pod status resolution when filtering by pod
// status (pageGroups=nil spans all groups under the user filter). When not
// filtering, status is computed page-scoped in the fan-out below.
gUp, gUpCtx := errgroup.WithContext(ctx)
gUp.Go(func() error {
var err error
metadataMap, err = m.getStatefulSetsTableMetadata(gUpCtx, orgID, req)
return err
})
if !filterByPodStatus.IsZero() {
gUp.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gUpCtx, orgID, req.Start, req.End, statefulSetFilter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if err := gUp.Wait(); err != nil {
metadataMap, err := m.getStatefulSetsTableMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
if !filterByPodStatus.IsZero() {
// Required metric missing: can't derive status, so surface the warning
// and return empty rather than silently filtering everything out.
if podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.StatefulSetRecord{}
resp.Total = 0
return resp, nil
}
// Secondary filter: keep only statefulsets with a status-matching pod.
metadataMap = intersectMap(metadataMap, podStatusCounts)
}
resp.Total = len(metadataMap)
// podStatusCounts is the status keyset (nil when not filtering); getTopStatefulSetGroups
// intersects the ranked groups against it before paginating.
pageGroups, err := m.getTopStatefulSetGroups(ctx, orgID, req, metadataMap, podStatusCounts)
pageGroups, err := m.getTopStatefulSetGroups(ctx, orgID, req, metadataMap)
if err != nil {
return nil, err
}
@@ -1267,8 +991,21 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newStatefulSetsTableListQuery())
// Pods owned by a StatefulSet carry k8s.statefulset.name as a resource attribute,
// so default-groupBy gives per-statefulset status counts automatically.
var (
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -1276,15 +1013,11 @@ func (m *module) ListStatefulSets(ctx context.Context, orgID valuer.UUID, req *i
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
return err
})
// When filtering, podStatusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if filterByPodStatus.IsZero() {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, statefulSetFilter, req.GroupBy, pageGroups, inframonitoringtypes.PodStatus{})
return err
})
}
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
if err := g.Wait(); err != nil {
return nil, err
@@ -1325,7 +1058,7 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
// Bake the jobs base filter into req.Filter so all downstream helpers pick it up.
if req.Filter == nil {
req.Filter = &inframonitoringtypes.JobFilter{}
req.Filter = &qbtypes.Filter{}
}
req.Filter.Expression = mergeFilterExpressions(jobsBaseFilterExpr, req.Filter.Expression)
@@ -1340,60 +1073,14 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
return resp, nil
}
var (
jobFilter *qbtypes.Filter
filterByPodStatus inframonitoringtypes.PodStatus
filterExpr string
metadataMap map[string]map[string]string
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
jobFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
// Metadata, plus a full-scope pod status resolution when filtering by pod
// status (pageGroups=nil spans all groups under the user filter). When not
// filtering, status is computed page-scoped in the fan-out below.
gUp, gUpCtx := errgroup.WithContext(ctx)
gUp.Go(func() error {
var err error
metadataMap, err = m.getJobsTableMetadata(gUpCtx, orgID, req)
return err
})
if !filterByPodStatus.IsZero() {
gUp.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gUpCtx, orgID, req.Start, req.End, jobFilter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if err := gUp.Wait(); err != nil {
metadataMap, err := m.getJobsTableMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
if !filterByPodStatus.IsZero() {
// Required metric missing: can't derive status, so surface the warning
// and return empty rather than silently filtering everything out.
if podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.JobRecord{}
resp.Total = 0
return resp, nil
}
// Secondary filter: keep only jobs with a status-matching pod.
metadataMap = intersectMap(metadataMap, podStatusCounts)
}
resp.Total = len(metadataMap)
// podStatusCounts is the status keyset (nil when not filtering); getTopJobGroups
// intersects the ranked groups against it before paginating.
pageGroups, err := m.getTopJobGroups(ctx, orgID, req, metadataMap, podStatusCounts)
pageGroups, err := m.getTopJobGroups(ctx, orgID, req, metadataMap)
if err != nil {
return nil, err
}
@@ -1403,8 +1090,21 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newJobsTableListQuery())
// Pods owned by a Job carry k8s.job.name as a resource attribute, so default-groupBy
// gives per-job status counts automatically.
var (
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -1412,15 +1112,11 @@ func (m *module) ListJobs(ctx context.Context, orgID valuer.UUID, req *inframoni
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
return err
})
// When filtering, podStatusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if filterByPodStatus.IsZero() {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, jobFilter, req.GroupBy, pageGroups, inframonitoringtypes.PodStatus{})
return err
})
}
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
if err := g.Wait(); err != nil {
return nil, err
@@ -1461,7 +1157,7 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
// Bake the workload base filter into req.Filter so all downstream helpers pick it up.
if req.Filter == nil {
req.Filter = &inframonitoringtypes.DaemonSetFilter{}
req.Filter = &qbtypes.Filter{}
}
req.Filter.Expression = mergeFilterExpressions(daemonSetsBaseFilterExpr, req.Filter.Expression)
@@ -1476,60 +1172,14 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
return resp, nil
}
var (
daemonSetFilter *qbtypes.Filter
filterByPodStatus inframonitoringtypes.PodStatus
filterExpr string
metadataMap map[string]map[string]string
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
if req.Filter != nil {
filterExpr = req.Filter.Expression
daemonSetFilter = &req.Filter.Filter
filterByPodStatus = req.Filter.FilterByPodStatus
}
// Metadata, plus a full-scope pod status resolution when filtering by pod
// status (pageGroups=nil spans all groups under the user filter). When not
// filtering, status is computed page-scoped in the fan-out below.
gUp, gUpCtx := errgroup.WithContext(ctx)
gUp.Go(func() error {
var err error
metadataMap, err = m.getDaemonSetsTableMetadata(gUpCtx, orgID, req)
return err
})
if !filterByPodStatus.IsZero() {
gUp.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gUpCtx, orgID, req.Start, req.End, daemonSetFilter, req.GroupBy, nil, filterByPodStatus)
return err
})
}
if err := gUp.Wait(); err != nil {
metadataMap, err := m.getDaemonSetsTableMetadata(ctx, orgID, req)
if err != nil {
return nil, err
}
if !filterByPodStatus.IsZero() {
// Required metric missing: can't derive status, so surface the warning
// and return empty rather than silently filtering everything out.
if podStatusWarning != nil {
resp.Warning = podStatusWarning
resp.Records = []inframonitoringtypes.DaemonSetRecord{}
resp.Total = 0
return resp, nil
}
// Secondary filter: keep only daemonsets with a status-matching pod.
metadataMap = intersectMap(metadataMap, podStatusCounts)
}
resp.Total = len(metadataMap)
// podStatusCounts is the status keyset (nil when not filtering); getTopDaemonSetGroups
// intersects the ranked groups against it before paginating.
pageGroups, err := m.getTopDaemonSetGroups(ctx, orgID, req, metadataMap, podStatusCounts)
pageGroups, err := m.getTopDaemonSetGroups(ctx, orgID, req, metadataMap)
if err != nil {
return nil, err
}
@@ -1539,8 +1189,21 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
return resp, nil
}
filterExpr := ""
if req.Filter != nil {
filterExpr = req.Filter.Expression
}
fullQueryReq := buildFullQueryRequest(req.Start, req.End, filterExpr, req.GroupBy, pageGroups, m.newDaemonSetsTableListQuery())
// Pods owned by a DaemonSet carry k8s.daemonset.name as a resource attribute,
// so default-groupBy gives per-daemonset status counts automatically.
var (
queryResp *qbtypes.QueryRangeResponse
podStatusCounts map[string]podStatusCounts
podStatusWarning *qbtypes.QueryWarnData
)
g, gCtx := errgroup.WithContext(ctx)
g.Go(func() error {
@@ -1548,15 +1211,11 @@ func (m *module) ListDaemonSets(ctx context.Context, orgID valuer.UUID, req *inf
queryResp, err = m.querier.QueryRange(gCtx, orgID, fullQueryReq)
return err
})
// When filtering, podStatusCounts already holds the full-scope map (a superset
// of the page); otherwise compute it page-scoped here.
if filterByPodStatus.IsZero() {
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, daemonSetFilter, req.GroupBy, pageGroups, inframonitoringtypes.PodStatus{})
return err
})
}
g.Go(func() error {
var err error
podStatusCounts, podStatusWarning, err = m.getPerGroupPodStatusCountsWithReqMetricChecks(gCtx, orgID, req.Start, req.End, req.Filter, req.GroupBy, pageGroups)
return err
})
if err := g.Wait(); err != nil {
return nil, err

View File

@@ -69,12 +69,9 @@ func (m *module) getTopNamespaceGroups(
orgID valuer.UUID,
req *inframonitoringtypes.PostableNamespaces,
metadataMap map[string]map[string]string,
podStatusCountsMap map[string]podStatusCounts,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.NamespaceNameAttrKey {
// metadataMap is already status-filtered by the caller, so the name
// branch needs no extra intersection.
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NamespaceNameAttrKey), nil
}
queryNamesForOrderBy := orderByToNamespacesQueryNames[orderByKey]
@@ -116,11 +113,6 @@ func (m *module) getTopNamespaceGroups(
}
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
// When filtering by status, intersect the ranked groups against the keyset —
// an empty map (no group matched) must yield an empty page, not the full set.
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() {
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
}
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
}
@@ -131,9 +123,5 @@ func (m *module) getNamespacesTableMetadata(ctx context.Context, orgID valuer.UU
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, namespacesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, namespacesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -96,13 +96,9 @@ func (m *module) getTopNodeGroups(
orgID valuer.UUID,
req *inframonitoringtypes.PostableNodes,
metadataMap map[string]map[string]string,
podStatusCountsMap map[string]podStatusCounts,
nodeConditionCountsMap map[string]nodeConditionCounts,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.NodeNameAttrKey {
// metadataMap is already status-filtered by the caller, so the name
// branch needs no extra intersection.
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.NodeNameAttrKey), nil
}
queryNamesForOrderBy := orderByToNodesQueryNames[orderByKey]
@@ -144,17 +140,6 @@ func (m *module) getTopNodeGroups(
}
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
// When filtering by a secondary status, intersect the ranked groups against
// its keyset — an empty map (no match) must yield an empty page, not the full
// set. Both filters compose as AND.
if req.Filter != nil {
if !req.Filter.FilterByPodStatus.IsZero() {
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
}
if !req.Filter.FilterByNodeReadiness.IsZero() {
allMetricGroups = intersectRankedGroups(allMetricGroups, nodeConditionCountsMap)
}
}
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
}
@@ -165,11 +150,7 @@ func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, r
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, nodesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, nodesTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}
// getPerGroupNodeConditionCounts computes per-group node counts bucketed by each
@@ -183,21 +164,6 @@ func (m *module) getNodesTableMetadata(ctx context.Context, orgID valuer.UUID, r
// countNodesPerCondition: per-group uniqExactIf into ready/not_ready buckets.
//
// Groups absent from the result map have implicit zero counts (caller default).
// nodeReadinessFilterClause returns the outer WHERE clause (and its arg) that
// restricts latest_condition_per_node to a single readiness. condition_value is
// numeric (1=Ready, 0=NotReady), so we map the enum to its int. Empty readiness
// returns no clause.
func nodeReadinessFilterClause(filterByNodeReadiness inframonitoringtypes.NodeCondition) (string, []any) {
if filterByNodeReadiness.IsZero() {
return "", nil
}
v := inframonitoringtypes.NodeConditionNumNotReady
if filterByNodeReadiness == inframonitoringtypes.NodeConditionReady {
v = inframonitoringtypes.NodeConditionNumReady
}
return " WHERE condition_value = ? ", []any{v}
}
func (m *module) getPerGroupNodeConditionCounts(
ctx context.Context,
orgID valuer.UUID,
@@ -205,11 +171,8 @@ func (m *module) getPerGroupNodeConditionCounts(
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
filterByNodeReadiness inframonitoringtypes.NodeCondition,
) (map[string]nodeConditionCounts, error) {
// Empty pageGroups means "span all under user filter", allowed only in
// full-scope mode (filtering by readiness). Otherwise it's an empty page.
if len(groupBy) == 0 || (len(pageGroups) == 0 && filterByNodeReadiness.IsZero()) {
if len(pageGroups) == 0 || len(groupBy) == 0 {
return map[string]nodeConditionCounts{}, nil
}
@@ -297,12 +260,9 @@ func (m *module) getPerGroupNodeConditionCounts(
fmt.Sprintf("uniqExactIf(node_name, condition_value = %d) AS ready_count", inframonitoringtypes.NodeConditionNumReady),
fmt.Sprintf("uniqExactIf(node_name, condition_value = %d) AS not_ready_count", inframonitoringtypes.NodeConditionNumNotReady),
)
// Push-down: keep only nodes whose readiness matches the requested one.
readinessWhereClause, readinessWhereArgs := nodeReadinessFilterClause(filterByNodeReadiness)
countNodesPerConditionSQL := fmt.Sprintf(
"SELECT %s FROM latest_condition_per_node%s GROUP BY %s",
"SELECT %s FROM latest_condition_per_node GROUP BY %s",
strings.Join(countNodesPerConditionSelectCols, ", "),
readinessWhereClause,
strings.Join(countNodesPerConditionGroupBy, ", "),
)
@@ -312,7 +272,7 @@ func (m *module) getPerGroupNodeConditionCounts(
fmt.Sprintf("latest_condition_per_node AS (%s)", latestConditionPerNodeSQL),
}
finalSQL := querybuilder.CombineCTEs(cteFragments) + countNodesPerConditionSQL
finalArgs := querybuilder.PrependArgs([][]any{timeSeriesFPsArgs, latestConditionPerNodeArgs}, readinessWhereArgs)
finalArgs := querybuilder.PrependArgs([][]any{timeSeriesFPsArgs, latestConditionPerNodeArgs}, nil)
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
if err != nil {

View File

@@ -1,48 +0,0 @@
package implinframonitoring
import (
"reflect"
"testing"
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
)
func TestNodeReadinessFilterClause(t *testing.T) {
tests := []struct {
name string
readiness inframonitoringtypes.NodeCondition
wantClause string
wantArgs []any
}{
{
name: "empty readiness yields no clause",
readiness: inframonitoringtypes.NodeCondition{},
wantClause: "",
wantArgs: nil,
},
{
name: "ready maps to 1",
readiness: inframonitoringtypes.NodeConditionReady,
wantClause: " WHERE condition_value = ? ",
wantArgs: []any{inframonitoringtypes.NodeConditionNumReady},
},
{
name: "not_ready maps to 0",
readiness: inframonitoringtypes.NodeConditionNotReady,
wantClause: " WHERE condition_value = ? ",
wantArgs: []any{inframonitoringtypes.NodeConditionNumNotReady},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotClause, gotArgs := nodeReadinessFilterClause(tt.readiness)
if gotClause != tt.wantClause {
t.Errorf("nodeReadinessFilterClause(%v) clause = %q, want %q", tt.readiness, gotClause, tt.wantClause)
}
if !reflect.DeepEqual(gotArgs, tt.wantArgs) {
t.Errorf("nodeReadinessFilterClause(%v) args = %v, want %v", tt.readiness, gotArgs, tt.wantArgs)
}
})
}
}

View File

@@ -150,12 +150,9 @@ func (m *module) getTopPodGroups(
orgID valuer.UUID,
req *inframonitoringtypes.PostablePods,
metadataMap map[string]map[string]string,
podStatusCountsMap map[string]podStatusCounts,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.PodNameAttrKey {
// metadataMap is already status-filtered by the caller, so the name
// branch needs no extra intersection.
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.PodNameAttrKey), nil
}
queryNamesForOrderBy := orderByToPodsQueryNames[orderByKey]
@@ -197,10 +194,6 @@ func (m *module) getTopPodGroups(
}
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
// When filtering by status, intersect the rankedGroup
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() {
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
}
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
}
@@ -211,11 +204,7 @@ func (m *module) getPodsTableMetadata(ctx context.Context, orgID valuer.UUID, re
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, podsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, podsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}
// getPerGroupPodStatusCountsWithReqMetricChecks gates getPerGroupPodStatusCounts
@@ -230,7 +219,6 @@ func (m *module) getPerGroupPodStatusCountsWithReqMetricChecks(
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
filterByPodStatus inframonitoringtypes.PodStatus,
) (map[string]podStatusCounts, *qbtypes.QueryWarnData, error) {
present, err := m.getMetricsExistence(ctx, podStatusMetricNamesList)
if err != nil {
@@ -256,24 +244,13 @@ func (m *module) getPerGroupPodStatusCountsWithReqMetricChecks(
return map[string]podStatusCounts{}, warning, nil
}
counts, err := m.getPerGroupPodStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups, filterByPodStatus)
counts, err := m.getPerGroupPodStatusCounts(ctx, orgID, start, end, filter, groupBy, pageGroups)
if err != nil {
return nil, nil, err
}
return counts, nil, nil
}
// podStatusFilterClause returns the outer WHERE clause (and its arg) that
// restricts pod_status to a single display status. valuer lowercases the wire
// value while display_status is kubectl-cased, so we compare lower() on both.
// Empty status returns no clause.
func podStatusFilterClause(filterByPodStatus inframonitoringtypes.PodStatus) (string, []any) {
if filterByPodStatus.IsZero() {
return "", nil
}
return " WHERE lower(display_status) = ? ", []any{filterByPodStatus.StringValue()}
}
// getPerGroupPodStatusCounts computes per-group pod counts bucketed by each
// pod's latest kubectl-style display status in the requested window. Caller
// must ensure the required metrics exist (getPerGroupPodStatusCountsWithReqMetricChecks).
@@ -294,10 +271,8 @@ func (m *module) getPerGroupPodStatusCounts(
filter *qbtypes.Filter,
groupBy []qbtypes.GroupByKey,
pageGroups []map[string]string,
filterByPodStatus inframonitoringtypes.PodStatus,
) (map[string]podStatusCounts, error) {
// return early if no group by or (no pagegroups provided plus no filterBystatus given for a full scan)
if len(groupBy) == 0 || (len(pageGroups) == 0 && filterByPodStatus.IsZero()) {
if len(pageGroups) == 0 || len(groupBy) == 0 {
return map[string]podStatusCounts{}, nil
}
@@ -533,12 +508,9 @@ func (m *module) getPerGroupPodStatusCounts(
countGroupBy = append(countGroupBy, col)
}
countSelectCols = append(countSelectCols, statusCountCols...)
// Push-down: keep only pods whose display status matches the requested one.
statusWhereClause, statusWhereArgs := podStatusFilterClause(filterByPodStatus)
countSQL := fmt.Sprintf(
"SELECT %s FROM pod_status%s GROUP BY %s",
"SELECT %s FROM pod_status GROUP BY %s",
strings.Join(countSelectCols, ", "),
statusWhereClause,
strings.Join(countGroupBy, ", "),
)
@@ -557,7 +529,7 @@ func (m *module) getPerGroupPodStatusCounts(
phaseFpsArgs, phasePerPodArgs,
podReasonFpsArgs, podReasonPerPodArgs,
containerReasonFpsArgs, containerInnerArgs,
}, statusWhereArgs)
}, nil)
rows, err := m.telemetryStore.ClickhouseDB().Query(ctx, finalSQL, finalArgs...)
if err != nil {

View File

@@ -1,48 +0,0 @@
package implinframonitoring
import (
"reflect"
"testing"
"github.com/SigNoz/signoz/pkg/types/inframonitoringtypes"
)
func TestPodStatusFilterClause(t *testing.T) {
tests := []struct {
name string
status inframonitoringtypes.PodStatus
wantClause string
wantArgs []any
}{
{
name: "empty status yields no clause",
status: inframonitoringtypes.PodStatus{},
wantClause: "",
wantArgs: nil,
},
{
name: "crashloopbackoff pushes lowercased arg",
status: inframonitoringtypes.PodStatusCrashLoopBackOff,
wantClause: " WHERE lower(display_status) = ? ",
wantArgs: []any{"crashloopbackoff"},
},
{
name: "running pushes lowercased arg",
status: inframonitoringtypes.PodStatusRunning,
wantClause: " WHERE lower(display_status) = ? ",
wantArgs: []any{"running"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotClause, gotArgs := podStatusFilterClause(tt.status)
if gotClause != tt.wantClause {
t.Errorf("podStatusFilterClause(%v) clause = %q, want %q", tt.status, gotClause, tt.wantClause)
}
if !reflect.DeepEqual(gotArgs, tt.wantArgs) {
t.Errorf("podStatusFilterClause(%v) args = %v, want %v", tt.status, gotArgs, tt.wantArgs)
}
})
}
}

View File

@@ -86,12 +86,9 @@ func (m *module) getTopStatefulSetGroups(
orgID valuer.UUID,
req *inframonitoringtypes.PostableStatefulSets,
metadataMap map[string]map[string]string,
podStatusCountsMap map[string]podStatusCounts,
) ([]map[string]string, error) {
orderByKey := req.OrderBy.Key.Name
if orderByKey == inframonitoringtypes.StatefulSetNameAttrKey {
// metadataMap is already status-filtered by the caller, so the name
// branch needs no extra intersection.
return inframonitoringtypes.PaginateMetadataByName(metadataMap, req.GroupBy, req.OrderBy.Direction, req.Offset, req.Limit, inframonitoringtypes.StatefulSetNameAttrKey), nil
}
queryNamesForOrderBy := orderByToStatefulSetsQueryNames[orderByKey]
@@ -133,11 +130,6 @@ func (m *module) getTopStatefulSetGroups(
}
allMetricGroups := parseAndSortGroups(resp, rankingQueryName, req.GroupBy, req.OrderBy.Direction)
// When filtering by status, intersect the ranked groups against the keyset —
// an empty map (no group matched) must yield an empty page, not the full set.
if req.Filter != nil && !req.Filter.FilterByPodStatus.IsZero() {
allMetricGroups = intersectRankedGroups(allMetricGroups, podStatusCountsMap)
}
return paginateWithBackfill(allMetricGroups, metadataMap, req.GroupBy, req.Offset, req.Limit), nil
}
@@ -148,9 +140,5 @@ func (m *module) getStatefulSetsTableMetadata(ctx context.Context, orgID valuer.
nonGroupByAttrs = append(nonGroupByAttrs, key)
}
}
var filter *qbtypes.Filter
if req.Filter != nil {
filter = &req.Filter.Filter
}
return m.getMetadata(ctx, orgID, statefulSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, filter, req.Start, req.End)
return m.getMetadata(ctx, orgID, statefulSetsTableMetricNamesList, req.GroupBy, nonGroupByAttrs, req.Filter, req.Start, req.End)
}

View File

@@ -70,7 +70,7 @@ func newProvider(
telemetryaudit.LogAttributeKeysTblName,
telemetryaudit.LogResourceKeysTblName,
telemetrymetadata.DBName,
telemetrymetadata.AttributesMetadataLocalTableName,
telemetrymetadata.AttributesMetadataTableName,
telemetrymetadata.ColumnEvolutionMetadataTableName,
flagger,
)

View File

@@ -222,6 +222,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddTagUniqueIndexFactory(sqlstore, sqlschema),
sqlmigration.NewAddTelemetryTuplesFactory(sqlstore),
sqlmigration.NewAddTagRelationRankFactory(sqlstore, sqlschema),
sqlmigration.NewScrubEmailChannelTransportFactory(sqlstore),
)
}

View File

@@ -447,7 +447,7 @@ func New(
telemetryaudit.LogAttributeKeysTblName,
telemetryaudit.LogResourceKeysTblName,
telemetrymetadata.DBName,
telemetrymetadata.AttributesMetadataLocalTableName,
telemetrymetadata.AttributesMetadataTableName,
telemetrymetadata.ColumnEvolutionMetadataTableName,
flagger,
)

View File

@@ -0,0 +1,263 @@
package sqlmigration
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"log/slog"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type scrubEmailChannelTransport struct {
sqlstore sqlstore.SQLStore
logger *slog.Logger
}
type alertmanagerConfigScrubRow struct {
bun.BaseModel `bun:"table:alertmanager_config"`
ID string `bun:"id"`
Config string `bun:"config"`
}
type notificationChannelScrubRow struct {
bun.BaseModel `bun:"table:notification_channel"`
ID string `bun:"id"`
Data string `bun:"data"`
}
var emailTransportKeys = []string{
"from",
"hello",
"smarthost",
"auth_username",
"auth_password",
"auth_password_file",
"auth_secret",
"auth_secret_file",
"auth_identity",
"require_tls",
"tls_config",
"force_implicit_tls",
}
var globalSMTPKeys = []string{
"smtp_from",
"smtp_hello",
"smtp_smarthost",
"smtp_auth_username",
"smtp_auth_password",
"smtp_auth_password_file",
"smtp_auth_secret",
"smtp_auth_secret_file",
"smtp_auth_identity",
"smtp_require_tls",
"smtp_tls_config",
"smtp_force_implicit_tls",
}
func NewScrubEmailChannelTransportFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(
factory.MustNewName("scrub_email_channel_transport"),
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &scrubEmailChannelTransport{sqlstore: sqlstore, logger: ps.Logger}, nil
},
)
}
func (migration *scrubEmailChannelTransport) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *scrubEmailChannelTransport) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
if err := migration.scrubConfigs(ctx, tx); err != nil {
return err
}
if err := migration.scrubChannels(ctx, tx); err != nil {
return err
}
return tx.Commit()
}
func (migration *scrubEmailChannelTransport) scrubConfigs(ctx context.Context, tx bun.Tx) error {
rows := make([]*alertmanagerConfigScrubRow, 0)
if err := tx.NewSelect().Model(&rows).Scan(ctx); err != nil {
return err
}
for _, row := range rows {
cfg := make(map[string]json.RawMessage)
if err := json.Unmarshal([]byte(row.Config), &cfg); err != nil {
migration.logger.WarnContext(ctx, "skipping alertmanager config with unreadable config", slog.String("config_id", row.ID), errors.Attr(err))
continue
}
changed := false
if globalRaw, ok := cfg["global"]; ok && string(globalRaw) != "null" {
global := make(map[string]json.RawMessage)
if err := json.Unmarshal(globalRaw, &global); err != nil {
migration.logger.WarnContext(ctx, "skipping alertmanager config with unreadable global", slog.String("config_id", row.ID), errors.Attr(err))
continue
}
if deleteKeys(global, globalSMTPKeys) {
newGlobal, err := json.Marshal(global)
if err != nil {
return err
}
cfg["global"] = newGlobal
changed = true
}
}
if receiversRaw, ok := cfg["receivers"]; ok && string(receiversRaw) != "null" {
receivers := make([]map[string]json.RawMessage, 0)
if err := json.Unmarshal(receiversRaw, &receivers); err != nil {
migration.logger.WarnContext(ctx, "skipping alertmanager config with unreadable receivers", slog.String("config_id", row.ID), errors.Attr(err))
continue
}
receiversChanged := false
for _, receiver := range receivers {
scrubbed, err := scrubEmailConfigs(receiver)
if err != nil {
return err
}
receiversChanged = receiversChanged || scrubbed
}
if receiversChanged {
newReceivers, err := json.Marshal(receivers)
if err != nil {
return err
}
cfg["receivers"] = newReceivers
changed = true
}
}
if !changed {
continue
}
newConfig, err := json.Marshal(cfg)
if err != nil {
return err
}
if _, err := tx.NewUpdate().
Model((*alertmanagerConfigScrubRow)(nil)).
Set("config = ?", string(newConfig)).
Set("hash = ?", fmt.Sprintf("%x", md5.Sum(newConfig))).
Where("id = ?", row.ID).
Exec(ctx); err != nil {
return err
}
}
return nil
}
func (migration *scrubEmailChannelTransport) scrubChannels(ctx context.Context, tx bun.Tx) error {
rows := make([]*notificationChannelScrubRow, 0)
if err := tx.NewSelect().Model(&rows).Where("type = ?", "email").Scan(ctx); err != nil {
return err
}
for _, row := range rows {
receiver := make(map[string]json.RawMessage)
if err := json.Unmarshal([]byte(row.Data), &receiver); err != nil {
migration.logger.WarnContext(ctx, "skipping notification channel with unreadable data", slog.String("channel_id", row.ID), errors.Attr(err))
continue
}
scrubbed, err := scrubEmailConfigs(receiver)
if err != nil {
return err
}
if !scrubbed {
continue
}
newData, err := json.Marshal(receiver)
if err != nil {
return err
}
if _, err := tx.NewUpdate().
Model((*notificationChannelScrubRow)(nil)).
Set("data = ?", string(newData)).
Where("id = ?", row.ID).
Exec(ctx); err != nil {
return err
}
}
return nil
}
func scrubEmailConfigs(receiver map[string]json.RawMessage) (bool, error) {
emailConfigsRaw, ok := receiver["email_configs"]
if !ok || string(emailConfigsRaw) == "null" {
return false, nil
}
emailConfigs := make([]map[string]json.RawMessage, 0)
if err := json.Unmarshal(emailConfigsRaw, &emailConfigs); err != nil {
return false, err
}
changed := false
for _, emailConfig := range emailConfigs {
changed = deleteKeys(emailConfig, emailTransportKeys) || changed
}
if !changed {
return false, nil
}
newEmailConfigs, err := json.Marshal(emailConfigs)
if err != nil {
return false, err
}
receiver["email_configs"] = newEmailConfigs
return true, nil
}
func deleteKeys(m map[string]json.RawMessage, keys []string) bool {
deleted := false
for _, key := range keys {
if _, ok := m[key]; ok {
delete(m, key)
deleted = true
}
}
return deleted
}
func (migration *scrubEmailChannelTransport) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -89,7 +89,8 @@ func (c *conditionBuilder) conditionForArrayFunction(
for i, v := range list {
vals[i] = legacyCoerceNeedle(v, elemType)
}
arrayCond := fmt.Sprintf("%s(%s, %s)", operator.FunctionName(), arrayExpr, sb.Var(vals))
// Pin the needle array type to the haystack; scalar fallback below coerces value-level.
arrayCond := fmt.Sprintf("%s(%s, %s)", operator.FunctionName(), arrayExpr, castNeedleArray(elemType, sb.Var(vals)))
if !hasScalar {
return arrayCond, nil
}
@@ -113,6 +114,27 @@ func (c *conditionBuilder) conditionForArrayFunction(
return fmt.Sprintf("(%s OR ifNull(%s, false))", arrayCond, sb.And(sb.E(scalarExpr, typedNeedle), scalarGuard)), nil
}
// castNeedleArray pins an Int64 needle array to Array(Int64) so it matches the Array(Nullable(Int64))
// haystack; without it a needle >= 2^32 binds as Array(UInt64) and hasAny/hasAll error (code 386).
func castNeedleArray(elemType telemetrytypes.FieldDataType, arg string) string {
if elemType == telemetrytypes.FieldDataTypeInt64 {
return fmt.Sprintf("CAST(%s AS Array(Int64))", arg)
}
return arg
}
// firstTokenSeparator returns the first char of s that hasToken treats as a token separator
// (anything other than an ASCII letter or digit), and whether one was found.
func firstTokenSeparator(s string) (string, bool) {
for _, r := range s {
isAlphaNum := (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
if !isAlphaNum {
return string(r), true
}
}
return "", false
}
// conditionForHasToken builds a hasToken full-text search over the body column, resolving the
// column from the key name + use_json_body flag.
func (c *conditionBuilder) conditionForHasToken(
@@ -129,11 +151,19 @@ func (c *conditionBuilder) conditionForHasToken(
}
// hasToken matches string tokens only.
if _, ok := needle.(string); !ok {
needleStr, ok := needle.(string)
if !ok {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput,
"function `hasToken` expects value parameter to be a string").WithUrl(hasTokenFunctionDocURL)
}
// A multi-token needle makes CH hasToken error (code 36); reject up front as a 400. Both modes flow here.
if sep, found := firstTokenSeparator(needleStr); found {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput,
"function `hasToken` matches a single whole token, but %q contains the separator %q; use a substring filter (e.g. `body CONTAINS '%s'`) to search across separators",
needleStr, sep, needleStr).WithUrl(hasTokenFunctionDocURL)
}
bodyJSONEnabled := c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(orgID))
if !bodyJSONEnabled {

View File

@@ -104,6 +104,23 @@ func TestFilterExprLogsBodyJSON(t *testing.T) {
expectedArgs: []any{int64(200), int64(200)},
expectedErrorContains: "",
},
{
// Big-int needle CAST to Array(Int64) to match the haystack (else 386).
category: "json",
query: `hasAny(body.ids, ['9007199254740993', '9007199254740994'])`,
shouldPass: true,
expectedQuery: `WHERE (hasAny(JSONExtract(JSON_QUERY(body, '$."ids"[*]'), 'Array(Nullable(Int64))'), CAST(? AS Array(Int64))) OR ifNull((JSONExtract(JSON_VALUE(body, '$."ids"'), 'Nullable(Int64)') IN (?, ?) AND JSONType(body, 'ids') NOT IN ('Array', 'Object', 'Null')), false))`,
expectedArgs: []any{[]any{int64(9007199254740993), int64(9007199254740994)}, int64(9007199254740993), int64(9007199254740994)},
expectedErrorContains: "",
},
{
category: "json",
query: `hasAll(body.ids, ['9007199254740993', '9007199254740994'])`,
shouldPass: true,
expectedQuery: `WHERE (hasAll(JSONExtract(JSON_QUERY(body, '$."ids"[*]'), 'Array(Nullable(Int64))'), CAST(? AS Array(Int64))) OR ifNull(((JSONExtract(JSON_VALUE(body, '$."ids"'), 'Nullable(Int64)') = ? AND JSONExtract(JSON_VALUE(body, '$."ids"'), 'Nullable(Int64)') = ?) AND JSONType(body, 'ids') NOT IN ('Array', 'Object', 'Null')), false))`,
expectedArgs: []any{[]any{int64(9007199254740993), int64(9007199254740994)}, int64(9007199254740993), int64(9007199254740994)},
expectedErrorContains: "",
},
{
category: "json",
query: "body.message = hello",

View File

@@ -1561,6 +1561,19 @@ func TestFilterExprLogs(t *testing.T) {
expectedArgs: []any{"download"},
expectedErrorContains: "function `hasToken` expects value parameter to be a string",
},
// A multi-token needle (separator/whitespace) is a clean 400, not a CH execution error.
{
category: "hasTokenUnderscoreNeedle",
query: "hasToken(body, \"user_id\")",
shouldPass: false,
expectedErrorContains: "function `hasToken` matches a single whole token",
},
{
category: "hasTokenWhitespaceNeedle",
query: "hasToken(body, \"production node\")",
shouldPass: false,
expectedErrorContains: "function `hasToken` matches a single whole token",
},
// extra / mis-shaped value arguments are rejected, not silently dropped.
{
category: "hasExtraArgs",

View File

@@ -96,8 +96,11 @@ func (c *conditionBuilder) conditionForKey(
fieldExpression, value = querybuilder.DataTypeCollisionHandledFieldName(key, value, fieldExpression, operator)
// key must exists to apply main filter
expr := `if(mapContains(%s, %s), %s, true)`
// key must exist to apply the main filter. for positive operators the
// absent-key rows are excluded (fallback false); for negative operators
// they are kept (fallback true) so rows legitimately lacking the key match.
keyMissingFallback := operator.IsNegativeOperator()
expr := `if(mapContains(%s, %s), %s, %t)`
var cond string
@@ -171,5 +174,5 @@ func (c *conditionBuilder) conditionForKey(
}
}
return fmt.Sprintf(expr, columns[0].Name, sb.Var(key.Name), cond), nil
return fmt.Sprintf(expr, columns[0].Name, sb.Var(key.Name), cond, keyMissingFallback), nil
}

View File

@@ -34,7 +34,7 @@ func TestConditionFor(t *testing.T) {
},
operator: qbtypes.FilterOperatorILike,
value: "%admin%",
expectedSQL: "WHERE if(mapContains(attributes, ?), LOWER(attributes['user.id']) LIKE LOWER(?), true)",
expectedSQL: "WHERE if(mapContains(attributes, ?), LOWER(attributes['user.id']) LIKE LOWER(?), false)",
expectedError: nil,
},
{
@@ -49,6 +49,150 @@ func TestConditionFor(t *testing.T) {
expectedSQL: "WHERE if(mapContains(attributes, ?), LOWER(attributes['user.id']) NOT LIKE LOWER(?), true)",
expectedError: nil,
},
{
name: "Equal operator - positive fallback false",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorEqual,
value: "admin",
expectedSQL: "WHERE if(mapContains(attributes, ?), attributes['user.id'] = ?, false)",
expectedError: nil,
},
{
name: "Not Equal operator - negative fallback true",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotEqual,
value: "admin",
expectedSQL: "WHERE if(mapContains(attributes, ?), attributes['user.id'] <> ?, true)",
expectedError: nil,
},
{
name: "In operator - positive fallback false",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorIn,
value: []any{"admin", "root"},
expectedSQL: "WHERE if(mapContains(attributes, ?), (attributes['user.id'] = ? OR attributes['user.id'] = ?), false)",
expectedError: nil,
},
{
name: "Not In operator - negative fallback true",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotIn,
value: []any{"admin", "root"},
expectedSQL: "WHERE if(mapContains(attributes, ?), (attributes['user.id'] <> ? AND attributes['user.id'] <> ?), true)",
expectedError: nil,
},
{
name: "Like operator - positive fallback false",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorLike,
value: "%admin%",
expectedSQL: "WHERE if(mapContains(attributes, ?), attributes['user.id'] LIKE ?, false)",
expectedError: nil,
},
{
name: "Not Like operator - negative fallback true",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotLike,
value: "%admin%",
expectedSQL: "WHERE if(mapContains(attributes, ?), attributes['user.id'] NOT LIKE ?, true)",
expectedError: nil,
},
{
name: "Contains operator - positive fallback false",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorContains,
value: "admin",
expectedSQL: "WHERE if(mapContains(attributes, ?), LOWER(attributes['user.id']) LIKE LOWER(?), false)",
expectedError: nil,
},
{
name: "Not Contains operator - negative fallback true",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotContains,
value: "admin",
expectedSQL: "WHERE if(mapContains(attributes, ?), LOWER(attributes['user.id']) NOT LIKE LOWER(?), true)",
expectedError: nil,
},
{
name: "Regexp operator - positive fallback false",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorRegexp,
value: "adm.*",
expectedSQL: "WHERE if(mapContains(attributes, ?), match(attributes['user.id'], ?), false)",
expectedError: nil,
},
{
name: "Not Regexp operator - negative fallback true",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotRegexp,
value: "adm.*",
expectedSQL: "WHERE if(mapContains(attributes, ?), NOT match(attributes['user.id'], ?), true)",
expectedError: nil,
},
{
name: "Exists operator - positive fallback false",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: "WHERE if(mapContains(attributes, ?), mapContains(attributes, 'user.id') = ?, false)",
expectedError: nil,
},
{
name: "Not Exists operator - negative fallback true",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotExists,
value: nil,
expectedSQL: "WHERE if(mapContains(attributes, ?), mapContains(attributes, 'user.id') <> ?, true)",
expectedError: nil,
},
}
for _, tc := range testCases {

View File

@@ -1436,6 +1436,11 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, orgID valuer.
sb.Where(sb.LE("unix_milli", fieldValueSelector.EndUnixMilli))
}
// scope to the requested signal's rows;
if fieldValueSelector.Signal != telemetrytypes.SignalUnspecified {
sb.Where(sb.E("data_source", fieldValueSelector.Signal.StringValue()))
}
if fieldValueSelector.Value != "" {
var conds []string
if fieldValueSelector.FieldContext != telemetrytypes.FieldContextAttribute &&
@@ -1464,9 +1469,9 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, orgID valuer.
}
if len(conds) != 0 {
// see `expr` in condition_builder.go, if key doesn't exist we don't check for value
// hence, this is join of conditions on resource and attributes
sb.Where(sb.And(conds...))
// the key may sit in the resource or attribute map (or both), so OR the
// two conditions — match if the key's value in either map contains searchText.
sb.Where(sb.Or(conds...))
}
}

View File

@@ -61,7 +61,7 @@ func TestGetFirstSeenFromMetricMetadata(t *testing.T) {
telemetryaudit.LogAttributeKeysTblName,
telemetryaudit.LogResourceKeysTblName,
DBName,
AttributesMetadataLocalTableName,
AttributesMetadataTableName,
ColumnEvolutionMetadataTableName,
flaggertest.New(t),
)

View File

@@ -10,6 +10,7 @@ import (
"strings"
"github.com/SigNoz/signoz/pkg/telemetrytraces"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
)
type migrateCommon struct {
@@ -23,119 +24,10 @@ func NewMigrateCommon(logger *slog.Logger) *migrateCommon {
}
}
// WrapInV5Envelope delegates to querybuildertypesv5.WrapInV5Envelope; the
// transform is stateless and shared with the v1→v2 dashboard conversion.
func (migration *migrateCommon) WrapInV5Envelope(name string, queryMap map[string]any, queryType string) map[string]any {
// Create a properly structured v5 query
v5Query := map[string]any{
"name": name,
"disabled": queryMap["disabled"],
"legend": queryMap["legend"],
}
if name != queryMap["expression"] {
// formula
queryType = "builder_formula"
v5Query["expression"] = queryMap["expression"]
if functions, ok := queryMap["functions"]; ok {
v5Query["functions"] = functions
}
return map[string]any{
"type": queryType,
"spec": v5Query,
}
}
// Add signal based on data source
if dataSource, ok := queryMap["dataSource"].(string); ok {
switch dataSource {
case "traces":
v5Query["signal"] = "traces"
case "logs":
v5Query["signal"] = "logs"
case "metrics":
v5Query["signal"] = "metrics"
}
}
if stepInterval, ok := queryMap["stepInterval"]; ok {
v5Query["stepInterval"] = stepInterval
}
if aggregations, ok := queryMap["aggregations"]; ok {
v5Query["aggregations"] = aggregations
}
if filter, ok := queryMap["filter"]; ok {
v5Query["filter"] = filter
}
// Copy groupBy with proper structure
if groupBy, ok := queryMap["groupBy"].([]any); ok {
v5GroupBy := make([]any, len(groupBy))
for i, gb := range groupBy {
if gbMap, ok := gb.(map[string]any); ok {
v5GroupBy[i] = map[string]any{
"name": gbMap["key"],
"fieldDataType": gbMap["dataType"],
"fieldContext": gbMap["type"],
}
}
}
v5Query["groupBy"] = v5GroupBy
}
// Copy orderBy with proper structure
if orderBy, ok := queryMap["orderBy"].([]any); ok {
v5OrderBy := make([]any, len(orderBy))
for i, ob := range orderBy {
if obMap, ok := ob.(map[string]any); ok {
v5OrderBy[i] = map[string]any{
"key": map[string]any{
"name": obMap["columnName"],
"fieldDataType": obMap["dataType"],
"fieldContext": obMap["type"],
},
"direction": obMap["order"],
}
}
}
v5Query["order"] = v5OrderBy
}
// Copy selectColumns as selectFields
if selectColumns, ok := queryMap["selectColumns"].([]any); ok {
v5SelectFields := make([]any, len(selectColumns))
for i, col := range selectColumns {
if colMap, ok := col.(map[string]any); ok {
v5SelectFields[i] = map[string]any{
"name": colMap["key"],
"fieldDataType": colMap["dataType"],
"fieldContext": colMap["type"],
}
}
}
v5Query["selectFields"] = v5SelectFields
}
// Copy limit and offset
if limit, ok := queryMap["limit"]; ok {
v5Query["limit"] = limit
}
if offset, ok := queryMap["offset"]; ok {
v5Query["offset"] = offset
}
if having, ok := queryMap["having"]; ok {
v5Query["having"] = having
}
if functions, ok := queryMap["functions"]; ok {
v5Query["functions"] = functions
}
return map[string]any{
"type": queryType,
"spec": v5Query,
}
return querybuildertypesv5.WrapInV5Envelope(name, queryMap, queryType)
}
func (mc *migrateCommon) updateQueryData(ctx context.Context, queryData map[string]any, version, widgetType string) bool {

View File

@@ -0,0 +1,353 @@
// nolint
package transition
import (
"context"
"log/slog"
)
// ══════════════════════════════════════════════
// Shape-safe (idempotent) migration
// ══════════════════════════════════════════════
//
// A copy of the Migrate → updateWidget → updateQueryData chain with the
// "uniformly v4 input" assumption removed, so it is safe on a dashboard whose
// `version` tag lies (a "v5"-labelled dashboard with un-upgraded, possibly mixed,
// bodies — the v1→v2 converter's case). Versus the original: no version gate, and
// each step acts only on the pre-v5 shape (leaving a v5 field alone), so it is
// idempotent. The original Migrate is left unchanged (battle-tested, no test net).
// The *ShapeSafe methods below each note the original they copy; the reused steps
// (createFilterExpression, fixGroupBy, buildAggregationExpression, orderByExpr) are
// already v5-safe.
// MigrateQueryDataShapeSafe is the per-query entry point (the core of
// updateQueryDataShapeSafe) for callers that process queries one at a time (the
// v1→v2 converter). widgetType is the v1 panelTypes (metric reduceTo on tables);
// "" is safe.
func (m *dashboardMigrateV5) MigrateQueryDataShapeSafe(ctx context.Context, queryData map[string]any, widgetType string) bool {
return m.updateQueryDataShapeSafe(ctx, queryData, widgetType)
}
// updateQueryDataShapeSafe copies updateQueryData, with each destructive step
// guarded to act only on the pre-v5 shape (see the file header).
func (mc *migrateCommon) updateQueryDataShapeSafe(ctx context.Context, queryData map[string]any, widgetType string) bool {
updated := false
aggregateOp, _ := queryData["aggregateOperator"].(string)
hasAggregation := aggregateOp != "" && aggregateOp != "noop"
if mc.createAggregationsShapeSafe(ctx, queryData, widgetType) {
updated = true
}
// createFilterExpression only touches v4 `filters`; skip if a v5 `filter` exists.
if _, hasFilter := queryData["filter"]; !hasFilter {
if mc.createFilterExpression(ctx, queryData) {
updated = true
}
}
if mc.fixGroupBy(queryData) {
updated = true
}
if mc.createHavingExpressionShapeSafe(queryData) {
updated = true
}
if hasAggregation {
if orderBy, ok := queryData["orderBy"].([]any); ok && orderByIsPreV5(orderBy) {
newOrderBy := make([]any, 0)
for _, order := range orderBy {
if orderMap, ok := order.(map[string]any); ok {
columnName, _ := orderMap["columnName"].(string)
// skip timestamp, id (logs, traces), samples(metrics) ordering for aggregation queries
if columnName != "timestamp" && columnName != "samples" && columnName != "id" {
if columnName == "#SIGNOZ_VALUE" {
if expr, has := mc.orderByExpr(queryData); has {
orderMap["columnName"] = expr
}
} else {
// if the order by key is not part of the group by keys, remove it
present := false
groupBy, ok := queryData["groupBy"].([]any)
if !ok {
return false
}
for idx := range groupBy {
item, ok := groupBy[idx].(map[string]any)
if !ok {
continue
}
key, ok := item["key"].(string)
if !ok {
continue
}
if key == columnName {
present = true
}
}
if !present {
mc.logger.WarnContext(ctx, "found a order by without group by, skipping", slog.String("order_col_name", columnName))
continue
}
}
newOrderBy = append(newOrderBy, orderMap)
}
}
}
queryData["orderBy"] = newOrderBy
updated = true
}
} else {
dataSource, _ := queryData["dataSource"].(string)
if orderBy, ok := queryData["orderBy"].([]any); ok && orderByIsPreV5(orderBy) {
newOrderBy := make([]any, 0)
for _, order := range orderBy {
if orderMap, ok := order.(map[string]any); ok {
columnName, _ := orderMap["columnName"].(string)
// skip id and timestamp for (traces)
if (columnName == "id" || columnName == "timestamp") && dataSource == "traces" {
mc.logger.InfoContext(ctx, "skipping `id` order by for traces")
continue
}
// skip id for (logs)
if (columnName == "id" || columnName == "timestamp") && dataSource == "logs" {
mc.logger.InfoContext(ctx, "skipping `id`/`timestamp` order by for logs")
continue
}
newOrderBy = append(newOrderBy, orderMap)
}
}
queryData["orderBy"] = newOrderBy
updated = true
}
}
// Only the `&& functionsArePreV5(functions)` guard differs from updateQueryData.
if functions, ok := queryData["functions"].([]any); ok && functionsArePreV5(functions) {
v5Functions := make([]any, len(functions))
for i, fn := range functions {
if fnMap, ok := fn.(map[string]any); ok {
v5Function := map[string]any{
"name": fnMap["name"],
}
// Convert args from v4 format to v5 FunctionArg format
if args, ok := fnMap["args"].([]any); ok {
v5Args := make([]any, len(args))
for j, arg := range args {
// In v4, args were just values. In v5, they are FunctionArg objects
v5Args[j] = map[string]any{
"name": "", // v4 didn't have named args
"value": arg,
}
}
v5Function["args"] = v5Args
}
// Handle namedArgs if present (some functions might have used this)
if namedArgs, ok := fnMap["namedArgs"].(map[string]any); ok {
// Convert named args to the new format
existingArgs, _ := v5Function["args"].([]any)
if existingArgs == nil {
existingArgs = []any{}
}
for name, value := range namedArgs {
existingArgs = append(existingArgs, map[string]any{
"name": name,
"value": value,
})
}
v5Function["args"] = existingArgs
}
v5Functions[i] = v5Function
}
}
queryData["functions"] = v5Functions
updated = true
}
delete(queryData, "aggregateOperator")
delete(queryData, "aggregateAttribute")
delete(queryData, "temporality")
delete(queryData, "timeAggregation")
delete(queryData, "spaceAggregation")
delete(queryData, "reduceTo")
delete(queryData, "filters")
delete(queryData, "ShiftBy")
delete(queryData, "IsAnomaly")
delete(queryData, "QueriesUsedInFormula")
delete(queryData, "seriesAggregation")
return updated
}
// createHavingExpressionShapeSafe copies createHavingExpression but leaves an
// already-v5 having:{expression} alone instead of wiping it.
func (mc *migrateCommon) createHavingExpressionShapeSafe(queryData map[string]any) bool {
if _, ok := queryData["having"].(map[string]any); ok {
return false // already v5-shaped
}
having, ok := queryData["having"].([]any)
if !ok || len(having) == 0 {
queryData["having"] = map[string]any{"expression": ""}
return true
}
dataSource, _ := queryData["dataSource"].(string)
for idx := range having {
if havingItem, ok := having[idx].(map[string]any); ok {
havingCol, has := mc.orderByExpr(queryData)
if has {
havingItem["columnName"] = havingCol
havingItem["key"] = map[string]any{"key": havingCol}
}
having[idx] = havingItem
}
}
queryData["having"] = map[string]any{"expression": mc.buildExpression(context.Background(), having, "AND", dataSource)}
return true
}
// createAggregationsShapeSafe copies createAggregations but skips a query that
// already has a v5 aggregations[], and picks the metric time/space aggregation
// from the body's shape (has timeAggregation/spaceAggregation?) rather than the
// version tag.
func (mc *migrateCommon) createAggregationsShapeSafe(ctx context.Context, queryData map[string]any, widgetType string) bool {
if aggs, ok := queryData["aggregations"].([]any); ok && len(aggs) > 0 {
return false // already v5-shaped
}
aggregateOp, hasOp := queryData["aggregateOperator"].(string)
aggregateAttr, hasAttr := queryData["aggregateAttribute"].(map[string]any)
dataSource, _ := queryData["dataSource"].(string)
if aggregateOp == "noop" && dataSource != "metrics" {
return false
}
if !hasOp || !hasAttr {
return false
}
var aggregation map[string]any
switch dataSource {
case "metrics":
_, hasTime := queryData["timeAggregation"]
_, hasSpace := queryData["spaceAggregation"]
if hasTime || hasSpace { // acts as a check for v4 shape: the body carries its own time/space aggregation.
if _, ok := queryData["spaceAggregation"]; !ok {
queryData["spaceAggregation"] = aggregateOp
}
aggregation = map[string]any{
"metricName": aggregateAttr["key"],
"temporality": queryData["temporality"],
"timeAggregation": queryData["timeAggregation"],
"spaceAggregation": queryData["spaceAggregation"],
}
if reduceTo, ok := queryData["reduceTo"].(string); ok {
aggregation["reduceTo"] = reduceTo
}
} else {
// v3 shape: derive time/space from the compound operator.
var timeAgg, spaceAgg, reduceTo string
switch aggregateOp {
case "sum_rate", "rate_sum":
timeAgg, spaceAgg, reduceTo = "rate", "sum", "sum"
case "avg_rate", "rate_avg":
timeAgg, spaceAgg, reduceTo = "rate", "avg", "avg"
case "min_rate", "rate_min":
timeAgg, spaceAgg, reduceTo = "rate", "min", "min"
case "max_rate", "rate_max":
timeAgg, spaceAgg, reduceTo = "rate", "max", "max"
case "hist_quantile_50":
timeAgg, spaceAgg, reduceTo = "", "p50", "avg"
case "hist_quantile_75":
timeAgg, spaceAgg, reduceTo = "", "p75", "avg"
case "hist_quantile_90":
timeAgg, spaceAgg, reduceTo = "", "p90", "avg"
case "hist_quantile_95":
timeAgg, spaceAgg, reduceTo = "", "p95", "avg"
case "hist_quantile_99":
timeAgg, spaceAgg, reduceTo = "", "p99", "avg"
case "rate":
timeAgg, spaceAgg, reduceTo = "rate", "sum", "sum"
case "p99", "p90", "p75", "p50", "p25", "p20", "p10", "p05":
mc.logger.InfoContext(ctx, "found invalid config")
timeAgg, spaceAgg, reduceTo = "avg", "avg", "avg"
case "min":
timeAgg, spaceAgg, reduceTo = "min", "min", "min"
case "max":
timeAgg, spaceAgg, reduceTo = "max", "max", "max"
case "avg":
timeAgg, spaceAgg, reduceTo = "avg", "avg", "avg"
case "sum":
timeAgg, spaceAgg, reduceTo = "sum", "sum", "sum"
case "count":
timeAgg, spaceAgg, reduceTo = "count", "sum", "sum"
case "count_distinct":
timeAgg, spaceAgg, reduceTo = "count_distinct", "sum", "sum"
case "noop":
mc.logger.WarnContext(ctx, "noop found in the aggregation data")
timeAgg, spaceAgg, reduceTo = "max", "max", "max"
}
aggregation = map[string]any{
"metricName": aggregateAttr["key"],
"temporality": queryData["temporality"],
"timeAggregation": timeAgg,
"spaceAggregation": spaceAgg,
}
if widgetType == "table" {
aggregation["reduceTo"] = reduceTo
} else if reduceTo, ok := queryData["reduceTo"].(string); ok {
aggregation["reduceTo"] = reduceTo
}
}
case "logs", "traces":
aggregation = map[string]any{"expression": mc.buildAggregationExpression(aggregateOp, aggregateAttr)}
default:
return false
}
queryData["aggregations"] = []any{aggregation}
return true
}
// orderByIsPreV5 reports whether an orderBy slice is still in the v4 shape (an
// entry carries "columnName"); a v5 orderBy uses {key:{name}, direction}.
func orderByIsPreV5(orderBy []any) bool {
for _, o := range orderBy {
if m, ok := o.(map[string]any); ok {
if _, has := m["columnName"]; has {
return true
}
}
}
return false
}
// functionsArePreV5 reports whether a functions slice is still in the v4 shape
// (args are raw values); a v5 function's args are {name,value} objects.
func functionsArePreV5(functions []any) bool {
for _, f := range functions {
if m, ok := f.(map[string]any); ok {
args, ok := m["args"].([]any)
if !ok || len(args) == 0 {
continue
}
_, argIsObject := args[0].(map[string]any)
return !argIsObject
}
}
return false
}

View File

@@ -35,12 +35,8 @@ func TestNewConfigFromChannels(t *testing.T) {
"email_configs": []any{map[string]any{
"send_resolved": false,
"to": "test@example.com",
"from": "alerts@example.com",
"hello": "localhost",
"smarthost": "smtp.example.com:587",
"require_tls": true,
"smarthost": "",
"html": "{{ template \"email.default.html\" . }}",
"tls_config": map[string]any{"insecure_skip_verify": false},
"threading": map[string]any{},
}},
},
@@ -63,7 +59,6 @@ func TestNewConfigFromChannels(t *testing.T) {
"slack_configs": []any{map[string]any{
"send_resolved": true,
"api_url": "https://slack.com/api/test",
"app_url": "https://slack.com/api/chat.postMessage",
"channel": "#alerts",
"callback_id": "{{ template \"slack.default.callbackid\" . }}",
"color": "{{ if eq .Status \"firing\" }}danger{{ else }}good{{ end }}",
@@ -77,12 +72,6 @@ func TestNewConfigFromChannels(t *testing.T) {
"title": "{{ template \"slack.default.title\" . }}",
"title_link": "{{ template \"slack.default.titlelink\" . }}",
"username": "{{ template \"slack.default.username\" . }}",
"http_config": map[string]any{
"tls_config": map[string]any{"insecure_skip_verify": false},
"follow_redirects": true,
"enable_http2": true,
"proxy_url": nil,
},
}},
},
},
@@ -104,7 +93,6 @@ func TestNewConfigFromChannels(t *testing.T) {
"pagerduty_configs": []any{map[string]any{
"send_resolved": false,
"service_key": "test",
"url": "https://events.pagerduty.com/v2/enqueue",
"client": "{{ template \"pagerduty.default.client\" . }}",
"client_url": "{{ template \"pagerduty.default.clientURL\" . }}",
"description": "{{ template \"pagerduty.default.description\" .}}",
@@ -116,12 +104,6 @@ func TestNewConfigFromChannels(t *testing.T) {
"num_resolved": "{{ .Alerts.Resolved | len }}",
"resolved": "{{ .Alerts.Resolved | toJson }}",
},
"http_config": map[string]any{
"tls_config": map[string]any{"insecure_skip_verify": false},
"follow_redirects": true,
"enable_http2": true,
"proxy_url": nil,
},
}},
},
},
@@ -148,7 +130,6 @@ func TestNewConfigFromChannels(t *testing.T) {
"pagerduty_configs": []any{map[string]any{
"send_resolved": false,
"service_key": "test",
"url": "https://events.pagerduty.com/v2/enqueue",
"client": "{{ template \"pagerduty.default.client\" . }}",
"client_url": "{{ template \"pagerduty.default.clientURL\" . }}",
"description": "{{ template \"pagerduty.default.description\" .}}",
@@ -160,12 +141,6 @@ func TestNewConfigFromChannels(t *testing.T) {
"num_resolved": "{{ .Alerts.Resolved | len }}",
"resolved": "{{ .Alerts.Resolved | toJson }}",
},
"http_config": map[string]any{
"tls_config": map[string]any{"insecure_skip_verify": false},
"follow_redirects": true,
"enable_http2": true,
"proxy_url": nil,
},
}},
},
{
@@ -173,7 +148,6 @@ func TestNewConfigFromChannels(t *testing.T) {
"slack_configs": []any{map[string]any{
"send_resolved": true,
"api_url": "https://slack.com/api/test",
"app_url": "https://slack.com/api/chat.postMessage",
"channel": "#alerts",
"callback_id": "{{ template \"slack.default.callbackid\" . }}",
"color": "{{ if eq .Status \"firing\" }}danger{{ else }}good{{ end }}",
@@ -187,12 +161,6 @@ func TestNewConfigFromChannels(t *testing.T) {
"title": "{{ template \"slack.default.title\" . }}",
"title_link": "{{ template \"slack.default.titlelink\" . }}",
"username": "{{ template \"slack.default.username\" . }}",
"http_config": map[string]any{
"tls_config": map[string]any{"insecure_skip_verify": false},
"follow_redirects": true,
"enable_http2": true,
"proxy_url": nil,
},
}},
},
},

View File

@@ -174,7 +174,7 @@ func newConfigFromString(s string) (*config.Config, map[string]customReceiverCon
return amConfig, customConfigs, nil
}
func newRawFromConfig(c *config.Config, customConfigs map[string]customReceiverConfigs) []byte {
func extendedReceivers(c *config.Config, customConfigs map[string]customReceiverConfigs) []*Receiver {
receivers := make([]*Receiver, len(c.Receivers))
for i := range c.Receivers {
base := c.Receivers[i]
@@ -185,7 +185,14 @@ func newRawFromConfig(c *config.Config, customConfigs map[string]customReceiverC
}
}
b, err := json.Marshal(storedConfig{Config: c, Receivers: receivers})
return receivers
}
func newRawFromConfig(c *config.Config, customConfigs map[string]customReceiverConfigs) []byte {
persistable := *c
persistable.Global = persistableGlobal(c.Global)
b, err := json.Marshal(storedConfig{Config: &persistable, Receivers: extendedReceivers(c, customConfigs)})
if err != nil {
// Taking inspiration from the upstream. This is never expected to happen.
return []byte(fmt.Sprintf("<error creating config string: %s>", err))
@@ -194,6 +201,28 @@ func newRawFromConfig(c *config.Config, customConfigs map[string]customReceiverC
return b
}
func persistableGlobal(g *config.GlobalConfig) *config.GlobalConfig {
if g == nil {
return nil
}
stripped := *g
stripped.SMTPFrom = ""
stripped.SMTPHello = ""
stripped.SMTPSmarthost = config.HostPort{}
stripped.SMTPAuthUsername = ""
stripped.SMTPAuthPassword = ""
stripped.SMTPAuthPasswordFile = ""
stripped.SMTPAuthSecret = ""
stripped.SMTPAuthSecretFile = ""
stripped.SMTPAuthIdentity = ""
stripped.SMTPRequireTLS = false
stripped.SMTPTLSConfig = nil
stripped.SMTPForceImplicitTLS = nil
return &stripped
}
func newConfigHash(s string) [16]byte {
return md5.Sum([]byte(s))
}
@@ -206,6 +235,37 @@ func (c *Config) flush() {
c.storeableConfig.UpdatedAt = time.Now()
}
func (c *Config) Resolved() (*Config, error) {
raw, err := json.Marshal(storedConfig{Config: c.alertmanagerConfig, Receivers: extendedReceivers(c.alertmanagerConfig, c.customConfigs)})
if err != nil {
return nil, err
}
alertmanagerConfig, customConfigs, err := newConfigFromString(string(raw))
if err != nil {
return nil, err
}
if err := alertmanagerConfig.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
return nil, err
}
storeableConfig := *c.storeableConfig
resolved := &Config{
alertmanagerConfig: alertmanagerConfig,
customConfigs: customConfigs,
storeableConfig: &storeableConfig,
}
resolved.applyNativeDefaults()
return resolved, nil
}
func (c *Config) validate() error {
_, err := c.Resolved()
return err
}
func (c *Config) CopyWithReset() (*Config, error) {
newConfig, err := NewDefaultConfig(
*c.alertmanagerConfig.Global,
@@ -271,6 +331,15 @@ func (c *Config) StoreableConfig() *StoreableConfig {
return c.storeableConfig
}
func cloneReceiver(receiver *Receiver) (*Receiver, error) {
raw, err := json.Marshal(receiver)
if err != nil {
return nil, err
}
return NewReceiver(string(raw))
}
func (c *Config) CreateReceiver(receiver *Receiver) error {
// check that receiver name is not already used
for _, existingReceiver := range c.alertmanagerConfig.Receivers {
@@ -279,16 +348,21 @@ func (c *Config) CreateReceiver(receiver *Receiver) error {
}
}
route, err := NewRouteFromReceiver(receiver)
owned, err := cloneReceiver(receiver)
if err != nil {
return err
}
route, err := NewRouteFromReceiver(owned)
if err != nil {
return err
}
c.alertmanagerConfig.Route.Routes = append(c.alertmanagerConfig.Route.Routes, route)
c.alertmanagerConfig.Receivers = append(c.alertmanagerConfig.Receivers, *receiver.Receiver)
c.setCustomConfigs(receiver)
c.alertmanagerConfig.Receivers = append(c.alertmanagerConfig.Receivers, *owned.Receiver)
c.setCustomConfigs(owned)
if err := c.alertmanagerConfig.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
if err := c.validate(); err != nil {
return err
}
c.applyNativeDefaults()
@@ -313,16 +387,21 @@ func (c *Config) GetReceiver(name string) (*Receiver, error) {
}
func (c *Config) UpdateReceiver(receiver *Receiver) error {
owned, err := cloneReceiver(receiver)
if err != nil {
return err
}
// find and update receiver
for i, existingReceiver := range c.alertmanagerConfig.Receivers {
if existingReceiver.Name == receiver.Name {
c.alertmanagerConfig.Receivers[i] = *receiver.Receiver
c.setCustomConfigs(receiver)
if existingReceiver.Name == owned.Name {
c.alertmanagerConfig.Receivers[i] = *owned.Receiver
c.setCustomConfigs(owned)
break
}
}
if err := c.alertmanagerConfig.UnmarshalYAML(func(i interface{}) error { return nil }); err != nil {
if err := c.validate(); err != nil {
return err
}
c.applyNativeDefaults()

View File

@@ -330,6 +330,120 @@ func TestSetGlobalConfigPreservesSMTPRequireTLS(t *testing.T) {
}
}
func newSMTPGlobalConfig() GlobalConfig {
return GlobalConfig{
SMTPFrom: "alerts@example.com",
SMTPHello: "example.com",
SMTPSmarthost: config.HostPort{Host: "smtp.sendgrid.net", Port: "587"},
SMTPAuthUsername: "apikey",
SMTPAuthPassword: "operator-secret",
SMTPRequireTLS: true,
}
}
func newEmailTestConfig(t *testing.T) *Config {
t.Helper()
cfg, err := NewDefaultConfig(
newSMTPGlobalConfig(),
RouteConfig{GroupInterval: time.Minute, GroupWait: time.Minute, RepeatInterval: time.Minute},
"1",
)
require.NoError(t, err)
receiver, err := NewReceiver(`{"name":"email-receiver","email_configs":[{"to":"team@example.com"}]}`)
require.NoError(t, err)
require.NoError(t, cfg.CreateReceiver(receiver))
return cfg
}
func TestStoreableConfigCarriesNoSMTPSettings(t *testing.T) {
cfg := newEmailTestConfig(t)
raw := cfg.StoreableConfig().Config
assert.NotContains(t, raw, "operator-secret")
assert.NotContains(t, raw, "smtp.sendgrid.net")
assert.NotContains(t, raw, "apikey")
assert.NotContains(t, raw, "alerts@example.com")
assert.Equal(t, "operator-secret", string(cfg.alertmanagerConfig.Global.SMTPAuthPassword))
}
func TestResolvedFillsEmailTransportFromGlobal(t *testing.T) {
cfg := newEmailTestConfig(t)
resolved, err := cfg.Resolved()
require.NoError(t, err)
receiver, err := resolved.GetReceiver("email-receiver")
require.NoError(t, err)
require.Len(t, receiver.EmailConfigs, 1)
got := receiver.EmailConfigs[0]
assert.Equal(t, "team@example.com", got.To)
assert.Equal(t, "smtp.sendgrid.net:587", got.Smarthost.String())
assert.Equal(t, "alerts@example.com", got.From)
assert.Equal(t, "apikey", got.AuthUsername)
assert.Equal(t, "operator-secret", string(got.AuthPassword))
require.NotNil(t, got.RequireTLS)
assert.True(t, *got.RequireTLS)
stored, err := cfg.GetReceiver("email-receiver")
require.NoError(t, err)
assert.Empty(t, stored.EmailConfigs[0].Smarthost.String())
assert.NotContains(t, cfg.StoreableConfig().Config, "operator-secret")
}
func TestStaleStoredSMTPSettingsAreReplacedOnLoad(t *testing.T) {
stored := &StoreableConfig{
Config: `{"global":{"resolve_timeout":"5m","smtp_from":"old@example.com","smtp_hello":"localhost","smtp_smarthost":"email-smtp.us-east-1.amazonaws.com:587","smtp_auth_username":"old-user","smtp_auth_password":"old-secret","smtp_require_tls":true},"route":{"receiver":"default-receiver","group_by":["ruleId"],"routes":[{"receiver":"email-receiver","continue":true,"matchers":["ruleId=~\"-1\""]}],"group_wait":"30s","group_interval":"5m","repeat_interval":"4h"},"receivers":[{"name":"default-receiver"},{"name":"email-receiver","email_configs":[{"send_resolved":false,"to":"team@example.com","from":"old@example.com","hello":"localhost","smarthost":"email-smtp.us-east-1.amazonaws.com:587","auth_username":"old-user","auth_password":"old-secret","require_tls":true}]}]}`,
OrgID: "1",
}
cfg, err := NewConfigFromStoreableConfig(stored)
require.NoError(t, err)
loaded, err := cfg.GetReceiver("email-receiver")
require.NoError(t, err)
require.Len(t, loaded.EmailConfigs, 1)
assert.Empty(t, loaded.EmailConfigs[0].Smarthost.String())
assert.Empty(t, string(loaded.EmailConfigs[0].AuthPassword))
require.NoError(t, cfg.SetGlobalConfig(newSMTPGlobalConfig()))
resolved, err := cfg.Resolved()
require.NoError(t, err)
receiver, err := resolved.GetReceiver("email-receiver")
require.NoError(t, err)
got := receiver.EmailConfigs[0]
assert.Equal(t, "smtp.sendgrid.net:587", got.Smarthost.String())
assert.Equal(t, "operator-secret", string(got.AuthPassword))
assert.Equal(t, "alerts@example.com", got.From)
assert.NotContains(t, cfg.StoreableConfig().Config, "old-secret")
assert.NotContains(t, cfg.StoreableConfig().Config, "amazonaws.com")
assert.NotContains(t, cfg.StoreableConfig().Config, "operator-secret")
}
func TestCreateReceiverDoesNotMutateCaller(t *testing.T) {
cfg := newEmailTestConfig(t)
resolved, err := cfg.Resolved()
require.NoError(t, err)
receiver, err := resolved.GetReceiver("email-receiver")
require.NoError(t, err)
require.Equal(t, "smtp.sendgrid.net:587", receiver.EmailConfigs[0].Smarthost.String())
throwaway, err := cfg.CopyWithReset()
require.NoError(t, err)
require.NoError(t, throwaway.CreateReceiver(receiver))
assert.Equal(t, "smtp.sendgrid.net:587", receiver.EmailConfigs[0].Smarthost.String())
assert.Equal(t, "operator-secret", string(receiver.EmailConfigs[0].AuthPassword))
}
// Round-trip: create → serialize → reload → GetReceiver still has the configs.
func TestConfigPreservesGoogleChatConfigs(t *testing.T) {
webhookURL, err := url.Parse("https://chat.googleapis.com/v1/spaces/test/messages")

View File

@@ -37,6 +37,7 @@ func NewReceiver(input string) (*Receiver, error) {
if err != nil {
return nil, err
}
stripEmailTransport(withDefaults)
receiver.Receiver = withDefaults
// Extend this block when adding another native notifier type.
@@ -51,6 +52,23 @@ func NewReceiver(input string) (*Receiver, error) {
return receiver, nil
}
func stripEmailTransport(base *config.Receiver) {
for _, ec := range base.EmailConfigs {
ec.From = ""
ec.Hello = ""
ec.Smarthost = config.HostPort{}
ec.AuthUsername = ""
ec.AuthPassword = ""
ec.AuthPasswordFile = ""
ec.AuthSecret = ""
ec.AuthSecretFile = ""
ec.AuthIdentity = ""
ec.RequireTLS = nil
ec.TLSConfig = nil
ec.ForceImplicitTLS = nil
}
}
func defaultedBaseReceiver(base *config.Receiver) (*config.Receiver, error) {
bytes, err := yaml.Marshal(base)
if err != nil {
@@ -100,7 +118,12 @@ func TestReceiver(ctx context.Context, receiver *Receiver, receiverIntegrationsF
return err
}
defaultedReceiver, err := testConfig.GetReceiver(receiver.Name)
resolvedConfig, err := testConfig.Resolved()
if err != nil {
return err
}
defaultedReceiver, err := resolvedConfig.GetReceiver(receiver.Name)
if err != nil {
return err
}

View File

@@ -46,6 +46,31 @@ func TestNewReceiver(t *testing.T) {
}
}
func TestNewReceiverStripsEmailTransport(t *testing.T) {
receiver, err := NewReceiver(`{"name":"email","email_configs":[{"to":"team@example.com","from":"attacker@example.com","hello":"example.com","smarthost":"smtp.example.com:587","auth_username":"user","auth_password":"supersecret","auth_secret":"alsosecret","auth_identity":"id","require_tls":false,"headers":{"Subject":"custom"}}]}`)
require.NoError(t, err)
require.Len(t, receiver.EmailConfigs, 1)
got := receiver.EmailConfigs[0]
assert.Equal(t, "team@example.com", got.To)
assert.Equal(t, map[string]string{"Subject": "custom"}, got.Headers)
assert.Empty(t, got.From)
assert.Empty(t, got.Hello)
assert.Empty(t, got.Smarthost.String())
assert.Empty(t, got.AuthUsername)
assert.Empty(t, string(got.AuthPassword))
assert.Empty(t, string(got.AuthSecret))
assert.Empty(t, got.AuthIdentity)
assert.Nil(t, got.RequireTLS)
assert.Nil(t, got.TLSConfig)
bytes, err := json.Marshal(receiver)
require.NoError(t, err)
assert.NotContains(t, string(bytes), "supersecret")
assert.NotContains(t, string(bytes), "smtp.example.com")
}
// Omitted fields fall back to DefaultGoogleChatReceiverConfig.
func TestNewReceiverGoogleChatAppliesDefaults(t *testing.T) {
receiver, err := NewReceiver(`{"name":"googlechat","googlechat_configs":[{"webhook_url":"https://chat.googleapis.com/v1/spaces/test/messages"}]}`)

View File

@@ -137,6 +137,13 @@ func NewAccountFromStorable(storableAccount *StorableCloudIntegration) (*Account
return nil, err
}
account.Config.Azure = azureConfig
case CloudProviderTypeGCP:
gcpConfig := new(GCPAccountConfig)
err := json.Unmarshal([]byte(storableAccount.Config), gcpConfig)
if err != nil {
return nil, err
}
account.Config.GCP = gcpConfig
}
if storableAccount.LastAgentReport != nil {

View File

@@ -7,7 +7,6 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/transition"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -22,6 +21,7 @@ var (
ErrCodeDashboardInvalidSource = errors.MustNewCode("dashboard_invalid_source")
ErrCodeDashboardImmutable = errors.MustNewCode("dashboard_immutable")
ErrCodeDashboardInvalidPatch = errors.MustNewCode("dashboard_invalid_patch")
ErrCodeDashboardMigrationFailed = errors.MustNewCode("dashboard_migration_failed")
)
type StorableDashboard struct {
@@ -413,27 +413,26 @@ func (dashboard *Dashboard) GetWidgetQuery(startTime, endTime, widgetIndex uint6
widgetData := data.Widgets[widgetIndex]
switch widgetData.Query.QueryType {
case "builder":
migrate := transition.NewMigrateCommon(logger)
for _, query := range widgetData.Query.Builder.QueryData {
queryName, ok := query["queryName"].(string)
if !ok {
return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string")
}
compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_query"))
compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_query"))
}
for _, query := range widgetData.Query.Builder.QueryFormulas {
queryName, ok := query["queryName"].(string)
if !ok {
return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string")
}
compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_formula"))
compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_formula"))
}
for _, query := range widgetData.Query.Builder.QueryTraceOperator {
queryName, ok := query["queryName"].(string)
if !ok {
return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string")
}
compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_trace_operator"))
compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_trace_operator"))
}
case "clickhouse_sql":
for _, query := range widgetData.Query.ClickhouseSQL {

View File

@@ -25,6 +25,15 @@ const (
dashboardNameSuffixLen = 8
)
const (
dashboardIconPathPrefix = "/assets/Icons/"
dashboardLogoPathPrefix = "/assets/Logos/"
)
// base64ImageDataURIRegex matches the only free-form image value we allow — a
// base64 image data URI. Mirrors the frontend resolver's allow-list.
var base64ImageDataURIRegex = regexp.MustCompile(`^data:image/(?:png|jpe?g|gif|webp|avif|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$`)
type DSLKey string
const (
@@ -180,6 +189,20 @@ type DashboardV2MetadataBase struct {
Image string `json:"image"`
}
func (m DashboardV2MetadataBase) validateImage() error {
if m.Image == "" {
return nil
}
if (strings.HasPrefix(m.Image, dashboardIconPathPrefix) && len(m.Image) > len(dashboardIconPathPrefix)) ||
(strings.HasPrefix(m.Image, dashboardLogoPathPrefix) && len(m.Image) > len(dashboardLogoPathPrefix)) {
return nil
}
if base64ImageDataURIRegex.MatchString(m.Image) {
return nil
}
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "image must be an %q or %q path, or a base64 image data URI", dashboardIconPathPrefix, dashboardLogoPathPrefix)
}
// ════════════════════════════════════════════════════════════════════════
// Postable
// ════════════════════════════════════════════════════════════════════════
@@ -239,6 +262,9 @@ func (p *PostableDashboardV2) Validate() error {
if err := validateDashboardTags(p.Tags); err != nil {
return err
}
if err := p.validateImage(); err != nil {
return err
}
return p.Spec.Validate()
}
@@ -409,6 +435,9 @@ func (u *UpdatableDashboardV2) Validate() error {
if err := validateDashboardTags(u.Tags); err != nil {
return err
}
if err := u.validateImage(); err != nil {
return err
}
return u.Spec.Validate()
}

View File

@@ -17,16 +17,17 @@ import (
// DashboardSpec is the SigNoz dashboard v2 spec shape. It mirrors
// dashboard.Spec (Perses) field-for-field, except every common.Plugin
// occurrence is replaced with a typed SigNoz plugin whose OpenAPI schema is a
// per-site discriminated oneOf.
// per-site discriminated oneOf. Perses's datasources field is deliberately
// dropped: SigNoz never reads it (queries carry their own signal/source), so
// the drift test allowlists it as an intentional omission.
type DashboardSpec struct {
Display Display `json:"display" required:"true"`
Datasources map[string]*DatasourceSpec `json:"datasources,omitzero"`
Variables []Variable `json:"variables" required:"true" nullable:"false"`
Panels map[string]*Panel `json:"panels" required:"true" nullable:"false"`
Layouts []Layout `json:"layouts" required:"true" nullable:"false"`
Duration common.DurationString `json:"duration"`
RefreshInterval common.DurationString `json:"refreshInterval"`
Links []Link `json:"links" required:"true" nullable:"false"`
Display Display `json:"display" required:"true"`
Variables []Variable `json:"variables" required:"true" nullable:"false"`
Panels map[string]*Panel `json:"panels" required:"true" nullable:"false"`
Layouts []Layout `json:"layouts" required:"true" nullable:"false"`
Duration common.DurationString `json:"duration"`
RefreshInterval common.DurationString `json:"refreshInterval"`
Links []Link `json:"links,omitzero"`
}
// ══════════════════════════════════════════════
@@ -45,16 +46,6 @@ func (d *DashboardSpec) UnmarshalJSON(data []byte) error {
return d.Validate()
}
// validateLinks rejects a missing/null spec.links value: a typed client must
// send [] rather than omitting links, so its value round-trips faithfully.
// Panel links are the panel spec's concern, validated in validatePanels.
func (d *DashboardSpec) validateLinks() error {
if d.Links == nil {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "spec.links is required; send [] when there are no links")
}
return nil
}
// ══════════════════════════════════════════════
// Cross-field validation
// ══════════════════════════════════════════════
@@ -63,9 +54,6 @@ func (d *DashboardSpec) Validate() error {
if err := d.Display.Validate("dashboard", "spec.display.name"); err != nil {
return err
}
if err := d.validateLinks(); err != nil {
return err
}
if err := d.validateVariables(); err != nil {
return err
}
@@ -117,12 +105,9 @@ func (d *DashboardSpec) validatePanels() error {
if err := panel.Spec.Display.Validate("panel", path+".spec.display.name"); err != nil {
return err
}
if err := panel.Spec.validateLinks(path); err != nil {
return err
}
panelKind := panel.Spec.Plugin.Kind
if len(panel.Spec.Queries) != 1 {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s.spec.queries: panel must have one query", path)
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s.spec.queries: panel must have one query, found %d", path, len(panel.Spec.Queries))
}
allowed := allowedQueryKinds[panelKind]
for qi, q := range panel.Spec.Queries {
@@ -285,8 +270,8 @@ func (d *DashboardSpec) validateLayouts() error {
return errors.NewInternalf(errors.CodeInternal, "spec.layouts[%d].spec: unexpected layout spec type %T", li, layout.Spec)
}
if grid.Display != nil {
if n := utf8.RuneCountInString(grid.Display.Title); n > MaxDisplayNameLen {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "spec.layouts[%d].spec.display.title: layout name must be at most %d characters, got %d", li, MaxDisplayNameLen, n)
if n := utf8.RuneCountInString(grid.Display.Title); n > MaxLayoutTitleLen {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "spec.layouts[%d].spec.display.title: layout name must be at most %d characters, got %d", li, MaxLayoutTitleLen, n)
}
}
if err := validateGridLayoutGeometry(grid, li); err != nil {

View File

@@ -0,0 +1,34 @@
package dashboardtypes
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDashboardV2MetadataBaseValidateImage(t *testing.T) {
valid := []string{
"", // no image → default
"/assets/Icons/eight-ball", // system icon path
"/assets/Icons/whatever", // unknown but well-formed icon path
"/assets/Logos/aws-dark", // logo path
"data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=", // base64 svg
"data:image/png;base64,iVBORw0KGgo=", // base64 png
}
for _, image := range valid {
assert.NoError(t, DashboardV2MetadataBase{Image: image}.validateImage(), "expected %q to be accepted", image)
}
invalid := []string{
"http://evil.com/x.svg", // remote URL
"javascript:alert(1)", // javascript URI
"data:text/html;base64,PHNjcmlwdD4=", // non-image data URI
"<svg onload=alert(1)>", // raw markup
"/assets/Icons/", // prefix with no name
"/assets/Logos/", // prefix with no name
"not-a-path", // plain string
}
for _, image := range invalid {
assert.Error(t, DashboardV2MetadataBase{Image: image}.validateImage(), "expected %q to be rejected", image)
}
}

View File

@@ -30,7 +30,7 @@ const basePostableJSON = `{
"spec": {
"name": "service",
"allowAllValue": true,
"allowMultiple": false,
"allowMultiple": true,
"plugin": {
"kind": "signoz/DynamicVariable",
"spec": {"name": "service.name", "signal": "metrics"}
@@ -152,12 +152,17 @@ func TestPatchableDashboardV2_Apply(t *testing.T) {
})
t.Run("add metadata image", func(t *testing.T) {
out, err := decode(t, `[{"op": "add", "path": "/image", "value": "https://example.com/img.png"}]`).Apply(base)
out, err := decode(t, `[{"op": "add", "path": "/image", "value": "/assets/Icons/eight-ball"}]`).Apply(base)
require.NoError(t, err)
assert.Equal(t, "https://example.com/img.png", out.Image)
assert.Equal(t, "/assets/Icons/eight-ball", out.Image)
assert.Equal(t, SchemaVersion, out.SchemaVersion, "schemaVersion preserved")
})
t.Run("reject non-icon/logo image", func(t *testing.T) {
_, err := decode(t, `[{"op": "add", "path": "/image", "value": "https://example.com/img.png"}]`).Apply(base)
assert.Error(t, err, "a URL image must be rejected")
})
t.Run("replace display name", func(t *testing.T) {
out, err := decode(t, `[{"op": "replace", "path": "/spec/display/name", "value": "Renamed"}]`).Apply(base)
require.NoError(t, err)

View File

@@ -44,7 +44,7 @@ func TestInvalidateNotAJSON(t *testing.T) {
// TestUnmarshalErrorPreservesNestedMessage guards the wrap on dec.Decode in
// DashboardSpec.UnmarshalJSON. The wrap stamps a consistent type/code on
// decode failures, but must not smother the rich messages produced by nested
// UnmarshalJSON methods (panel/query/variable/datasource plugin envelopes).
// UnmarshalJSON methods (panel/query/variable plugin envelopes).
func TestUnmarshalErrorPreservesNestedMessage(t *testing.T) {
data := []byte(`{
"panels": {
@@ -77,8 +77,8 @@ func TestUnmarshalErrorPreservesNestedMessage(t *testing.T) {
}
func TestValidateEmptySpec(t *testing.T) {
// no variables no panels
data := []byte(`{"links": []}`)
// no variables no panels no links
data := []byte(`{}`)
_, err := unmarshalDashboard(data)
assert.NoError(t, err, "expected valid")
}
@@ -91,7 +91,7 @@ func TestValidateOnlyVariables(t *testing.T) {
"spec": {
"name": "service",
"allowAllValue": true,
"allowMultiple": false,
"allowMultiple": true,
"plugin": {
"kind": "signoz/DynamicVariable",
"spec": {
@@ -237,6 +237,12 @@ func TestInvalidateListVariableCrossFields(t *testing.T) {
assert.Contains(t, err.Error(), "customAllValue cannot be set")
})
t.Run("allowAllValue without allowMultiple", func(t *testing.T) {
_, err := unmarshalDashboard(listVar(`"allowAllValue": true, "allowMultiple": false,`))
require.Error(t, err)
assert.Contains(t, err.Error(), "allowAllValue cannot be set")
})
t.Run("list defaultValue without allowMultiple", func(t *testing.T) {
_, err := unmarshalDashboard(listVar(`"allowAllValue": false, "allowMultiple": false, "defaultValue": ["a", "b"],`))
require.Error(t, err)
@@ -441,20 +447,6 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
}`,
wantContain: "FakeVariable",
},
{
name: "unknown datasource plugin",
data: `{
"datasources": {
"ds1": {
"default": true,
"plugin": {"kind": "FakeDatasource", "spec": {}}
}
},
"links": [],
"layouts": []
}`,
wantContain: "FakeDatasource",
},
}
for _, tt := range tests {
@@ -1739,55 +1731,61 @@ func TestInvalidateDuplicatePanelReference(t *testing.T) {
assert.Contains(t, err.Error(), "spec.layouts[0].spec.items[1].content")
}
// Every display name — dashboard, panel, variable — and the grid layout title is
// bounded at MaxDisplayNameLen. The name is one over the limit in each case, and
// the message reads "<json path>: <field> name must be at most ...", pairing the
// locatable path (like the other spec errors) with a human field label.
// Every display name — dashboard, panel, variable — is bounded at MaxDisplayNameLen,
// while the grid layout title has its own, larger bound (MaxLayoutTitleLen). The name
// is one over the relevant limit in each case, and the message reads "<json path>:
// <field> name must be at most ...", pairing the locatable path (like the other spec
// errors) with a human field label.
func TestInvalidateDisplayNameTooLong(t *testing.T) {
tooLong := strings.Repeat("x", MaxDisplayNameLen+1)
lengthMsg := fmt.Sprintf("must be at most %d characters, got %d", MaxDisplayNameLen, MaxDisplayNameLen+1)
testCases := []struct {
scenario string
dashboardJSON string
expectedPath string
expectedLabel string
scenario string
limit int
dashboardJSONFmt string
expectedPath string
expectedLabel string
}{
{
scenario: "dashboard display name",
dashboardJSON: `{"display": {"name": "` + tooLong + `"}, "links": [], "layouts": []}`,
expectedLabel: "dashboard",
expectedPath: "spec.display.name",
scenario: "dashboard display name",
limit: MaxDisplayNameLen,
dashboardJSONFmt: `{"display": {"name": "%s"}, "links": [], "layouts": []}`,
expectedLabel: "dashboard",
expectedPath: "spec.display.name",
},
{
scenario: "panel display name",
dashboardJSON: `{"panels": {"p1": {"kind": "Panel", "spec": {"links": [],"display": {"name": "` + tooLong + `"}, "plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": []}}}, "links": [], "layouts": []}`,
expectedLabel: "panel",
expectedPath: "spec.panels.p1.spec.display.name",
scenario: "panel display name",
limit: MaxDisplayNameLen,
dashboardJSONFmt: `{"panels": {"p1": {"kind": "Panel", "spec": {"links": [], "display": {"name": "%s"}, "plugin": {"kind": "signoz/TablePanel", "spec": {}}, "queries": []}}}, "links": [], "layouts": []}`,
expectedLabel: "panel",
expectedPath: "spec.panels.p1.spec.display.name",
},
{
scenario: "list variable display name",
dashboardJSON: `{"variables": [{"kind": "ListVariable", "spec": {"name": "svc", "display": {"name": "` + tooLong + `"}, "plugin": {"kind": "signoz/DynamicVariable", "spec": {"name": "service.name", "signal": "metrics"}}}}], "links": [], "layouts": []}`,
expectedLabel: "variable",
expectedPath: "spec.variables[0].spec.display.name",
scenario: "list variable display name",
limit: MaxDisplayNameLen,
dashboardJSONFmt: `{"variables": [{"kind": "ListVariable", "spec": {"name": "svc", "display": {"name": "%s"}, "plugin": {"kind": "signoz/DynamicVariable", "spec": {"name": "service.name", "signal": "metrics"}}}}], "links": [], "layouts": []}`,
expectedLabel: "variable",
expectedPath: "spec.variables[0].spec.display.name",
},
{
scenario: "text variable display name",
dashboardJSON: `{"variables": [{"kind": "TextVariable", "spec": {"name": "mytext", "value": "v", "display": {"name": "` + tooLong + `"}}}], "links": [], "layouts": []}`,
expectedLabel: "variable",
expectedPath: "spec.variables[0].spec.display.name",
scenario: "text variable display name",
limit: MaxDisplayNameLen,
dashboardJSONFmt: `{"variables": [{"kind": "TextVariable", "spec": {"name": "mytext", "value": "v", "display": {"name": "%s"}}}], "links": [], "layouts": []}`,
expectedLabel: "variable",
expectedPath: "spec.variables[0].spec.display.name",
},
{
scenario: "layout title",
dashboardJSON: `{"links": [], "layouts": [{"kind": "Grid", "spec": {"display": {"title": "` + tooLong + `"}, "items": []}}]}`,
expectedLabel: "layout",
expectedPath: "spec.layouts[0].spec.display.title",
scenario: "layout title",
limit: MaxLayoutTitleLen,
dashboardJSONFmt: `{"links": [], "layouts": [{"kind": "Grid", "spec": {"display": {"title": "%s"}, "items": []}}]}`,
expectedLabel: "layout",
expectedPath: "spec.layouts[0].spec.display.title",
},
}
for _, testCase := range testCases {
t.Run(testCase.scenario, func(t *testing.T) {
_, err := unmarshalDashboard([]byte(testCase.dashboardJSON))
tooLong := strings.Repeat("x", testCase.limit+1)
lengthMsg := fmt.Sprintf("must be at most %d characters, got %d", testCase.limit, testCase.limit+1)
_, err := unmarshalDashboard(fmt.Appendf(nil, testCase.dashboardJSONFmt, tooLong))
require.Error(t, err)
// Message is "<path>: <label> name must be at most N characters, got M".
want := testCase.expectedPath + ": " + testCase.expectedLabel + " name " + lengthMsg

View File

@@ -6,12 +6,12 @@ package dashboardtypes
import (
"reflect"
"slices"
"sort"
"strings"
"testing"
"github.com/perses/spec/go/dashboard"
"github.com/perses/spec/go/datasource"
"github.com/stretchr/testify/assert"
)
@@ -21,22 +21,27 @@ func TestDashboardSpecMatchesPerses(t *testing.T) {
name string
ours reflect.Type
perses reflect.Type
// omit lists json fields intentionally dropped from our type that Perses
// still has, so their absence is not reported as drift.
omit []string
}{
{"DashboardSpec", typeOf[DashboardSpec](), typeOf[dashboard.Spec]()},
{"Panel", typeOf[Panel](), typeOf[dashboard.Panel]()},
{"PanelSpec", typeOf[PanelSpec](), typeOf[dashboard.PanelSpec]()},
{"Query", typeOf[Query](), typeOf[dashboard.Query]()},
{"QuerySpec", typeOf[QuerySpec](), typeOf[dashboard.QuerySpec]()},
{"DatasourceSpec", typeOf[DatasourceSpec](), typeOf[datasource.Spec]()},
{"Variable", typeOf[Variable](), typeOf[dashboard.Variable]()},
{"ListVariableSpec", typeOf[ListVariableSpec](), typeOf[dashboard.ListVariableSpec]()},
{"TextVariableSpec", typeOf[TextVariableSpec](), typeOf[dashboard.TextVariableSpec]()},
{"Layout", typeOf[Layout](), typeOf[dashboard.Layout]()},
{name: "DashboardSpec", ours: typeOf[DashboardSpec](), perses: typeOf[dashboard.Spec](), omit: []string{"datasources"}},
{name: "Panel", ours: typeOf[Panel](), perses: typeOf[dashboard.Panel]()},
{name: "PanelSpec", ours: typeOf[PanelSpec](), perses: typeOf[dashboard.PanelSpec]()},
{name: "Query", ours: typeOf[Query](), perses: typeOf[dashboard.Query]()},
{name: "QuerySpec", ours: typeOf[QuerySpec](), perses: typeOf[dashboard.QuerySpec]()},
{name: "Variable", ours: typeOf[Variable](), perses: typeOf[dashboard.Variable]()},
{name: "ListVariableSpec", ours: typeOf[ListVariableSpec](), perses: typeOf[dashboard.ListVariableSpec]()},
{name: "TextVariableSpec", ours: typeOf[TextVariableSpec](), perses: typeOf[dashboard.TextVariableSpec]()},
{name: "Layout", ours: typeOf[Layout](), perses: typeOf[dashboard.Layout]()},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
missing, extra := drift(c.ours, c.perses)
for _, f := range c.omit {
missing = slices.DeleteFunc(missing, func(m string) bool { return m == f })
}
assert.Empty(t, missing,
"DashboardSpec (%s) is missing json fields present on Perses %s — upstream likely added or renamed a field",

View File

@@ -215,54 +215,6 @@ func (v VariablePluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error
return restrictKindToOneValue(s, v.Kind)
}
// ══════════════════════════════════════════════
// Datasource plugin
// ══════════════════════════════════════════════
type DatasourcePlugin struct {
Kind DatasourcePluginKind `json:"kind" required:"true"`
Spec any `json:"spec" required:"true"`
}
func (DatasourcePlugin) PrepareJSONSchema(s *jsonschema.Schema) error {
return markDiscriminator(s, "kind", map[string]string{
string(DatasourceKindSigNoz): schemaRef("DashboardtypesDatasourcePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesSigNozDatasourceSpec"),
})
}
func (p *DatasourcePlugin) UnmarshalJSON(data []byte) error {
kind, specJSON, err := extractKindAndSpec(data)
if err != nil {
return err
}
factory, ok := datasourcePluginSpecs[DatasourcePluginKind(kind)]
if !ok {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "unknown datasource plugin kind %q; allowed values: %s", kind, allowedValuesForKind(slices.Sorted(maps.Keys(datasourcePluginSpecs))))
}
spec, err := decodeSpec(specJSON, factory(), kind)
if err != nil {
return err
}
p.Kind = DatasourcePluginKind(kind)
p.Spec = *spec
return nil
}
func (DatasourcePlugin) JSONSchemaOneOf() []any {
return []any{
DatasourcePluginVariant[SigNozDatasourceSpec]{Kind: string(DatasourceKindSigNoz)},
}
}
type DatasourcePluginVariant[S any] struct {
Kind string `json:"kind" required:"true"`
Spec S `json:"spec" required:"true"`
}
func (v DatasourcePluginVariant[S]) PrepareJSONSchema(s *jsonschema.Schema) error {
return restrictKindToOneValue(s, v.Kind)
}
// ══════════════════════════════════════════════
// Helpers
// ══════════════════════════════════════════════
@@ -290,10 +242,6 @@ var (
VariableKindQuery: func() any { return new(QueryVariableSpec) },
VariableKindCustom: func() any { return new(CustomVariableSpec) },
}
datasourcePluginSpecs = map[DatasourcePluginKind]func() any{
DatasourceKindSigNoz: func() any { return new(SigNozDatasourceSpec) },
}
allowedQueryKinds = map[PanelPluginKind][]QueryPluginKind{
PanelKindTimeSeries: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},
PanelKindBarChart: {QueryKindBuilder, QueryKindComposite, QueryKindFormula, QueryKindTraceOperator, QueryKindPromQL, QueryKindClickHouseSQL},

View File

@@ -16,10 +16,14 @@ import (
"github.com/swaggest/jsonschema-go"
)
// MaxDisplayNameLen bounds every human-readable display name — dashboard, panel,
// and variable display names, plus the grid layout title.
// MaxDisplayNameLen bounds the human-readable display names — dashboard, panel,
// and variable. The grid layout title has its own, larger bound (MaxLayoutTitleLen).
const MaxDisplayNameLen = 128
// MaxLayoutTitleLen bounds a grid layout title. It is larger than MaxDisplayNameLen
// because v1 section (row) titles ran longer.
const MaxLayoutTitleLen = 256
type Display struct {
Name string `json:"name" required:"true"`
// Description always serializes ("" included) so a create -> GET round-trip
@@ -34,16 +38,6 @@ func (d Display) Validate(label, path string) error {
return nil
}
// ══════════════════════════════════════════════
// Datasource
// ══════════════════════════════════════════════
type DatasourceSpec struct {
Display *common.Display `json:"display,omitempty"`
Default bool `json:"default"`
Plugin DatasourcePlugin `json:"plugin"`
}
// ══════════════════════════════════════════════
// Panel
// ══════════════════════════════════════════════
@@ -78,17 +72,7 @@ type PanelSpec struct {
Display Display `json:"display" required:"true"`
Plugin PanelPlugin `json:"plugin" required:"true"`
Queries []Query `json:"queries" required:"true" nullable:"false"`
Links []Link `json:"links" required:"true" nullable:"false"`
}
// validateLinks rejects a missing/null links field, where path is the panel's
// location (e.g. "spec.panels.<key>"). A typed client must send [] rather than
// omitting links, so its value round-trips faithfully.
func (s *PanelSpec) validateLinks(path string) error {
if s.Links == nil {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s.spec.links is required; send [] when there are no links", path)
}
return nil
Links []Link `json:"links,omitzero"`
}
// Link replicates dashboard.Link (Perses) so its zero-valued fields survive the
@@ -241,6 +225,9 @@ func (s *ListVariableSpec) validate(path string) error {
if s.CustomAllValue != "" && !s.AllowAllValue {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s: customAllValue cannot be set if allowAllValue is not set to true", path)
}
if s.AllowAllValue && !s.AllowMultiple {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s: allowAllValue cannot be set if allowMultiple is not set to true", path)
}
if s.DefaultValue != nil && len(s.DefaultValue.SliceValues) > 0 && !s.AllowMultiple {
return errors.NewInvalidInputf(ErrCodeDashboardInvalidInput, "%s: defaultValue cannot be a list if allowMultiple is not set to true", path)
}

View File

@@ -179,21 +179,6 @@ func (PanelPluginKind) Enum() []any {
return []any{PanelKindTimeSeries, PanelKindBarChart, PanelKindNumber, PanelKindPieChart, PanelKindTable, PanelKindHistogram, PanelKindList}
}
type DatasourcePluginKind string
const (
DatasourceKindSigNoz DatasourcePluginKind = "signoz/Datasource"
)
func (DatasourcePluginKind) Enum() []any {
return []any{DatasourceKindSigNoz}
}
// SigNozDatasourceSpec is the (empty) signoz/Datasource plugin spec. Naming the
// type gives the variant a concrete, non-nullable spec schema instead of an
// inline free-form one.
type SigNozDatasourceSpec struct{}
type TimeSeriesPanelSpec struct {
Visualization TimeSeriesVisualization `json:"visualization"`
Formatting PanelFormatting `json:"formatting"`

View File

@@ -0,0 +1,103 @@
package dashboardtypes
import (
"encoding/json"
"github.com/SigNoz/signoz/pkg/errors"
)
// V1 → V2 migration. The v1 storable shape is the frontend's `DashboardData`
// (see frontend/src/types/api/dashboard/getAll.ts); v2 is DashboardV2 /
// DashboardSpec.
//
// Assumes the v1 widget query data has already been migrated to v5 shape
// (transition.dashboardMigrateV5). Pre-v5 builder queries will produce
// invalid v2 envelopes — run the v4→v5 migration first.
//
// The conversion is split across sibling files by concern:
// - perses_v1_to_v2_tags.go tags
// - perses_v1_to_v2_panels.go widgets → panels (+ panel field mappers)
// - perses_v1_to_v2_queries.go widget queries
// - perses_v1_to_v2_layouts.go grid layouts and sections
// - perses_v1_to_v2_variables.go variables
// - perses_v1_to_v2_decoder.go v1Decoder: typed field reads + malformed-field detection
// ══════════════════════════════════════════════
// Entry point
// ══════════════════════════════════════════════
func (storable StorableDashboard) IsV2() bool {
metadata, _ := storable.Data["metadata"].(map[string]any)
if metadata == nil {
return false
}
version, _ := metadata["schemaVersion"].(string)
return version == SchemaVersion
}
func (storable StorableDashboard) ConvertV1ToV2() (result *DashboardV2, err error) {
// Legacy v1 data can be arbitrarily malformed. The accessors degrade
// gracefully, but recover from any unforeseen panic so one bad dashboard
// surfaces as an error (to be logged and skipped) rather than crashing the run.
defer func() {
if r := recover(); r != nil {
result, err = nil, errors.Newf(errors.TypeInternal, ErrCodeDashboardMigrationFailed, "panic converting dashboard %s: %v", storable.ID, r)
}
}()
if storable.IsV2() {
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardMigrationFailed, "dashboard %s is already in %s schema", storable.ID, SchemaVersion)
}
d := &v1Decoder{}
title := d.readString(storable.Data, "title")
description := d.readString(storable.Data, "description")
image := resolveV1Image(d.readString(storable.Data, "image"))
sanitizeWidgetIDs(storable.Data)
panels := d.convertV1Panels(retainPlacedWidgets(storable.Data))
spec := DashboardSpec{
Display: Display{Name: clipName(title, MaxDisplayNameLen), Description: description},
Variables: d.convertV1Variables(storable.Data["variables"]),
Panels: panels,
Layouts: d.convertV1Layouts(storable.Data, panels),
// v1 has no dashboard links; emit [] (not nil) so the required field round-trips.
Links: []Link{},
}
// marshal and unmarshal cycle to confirm full validation
raw, marshalErr := json.Marshal(spec)
if marshalErr != nil {
return nil, errors.WrapInternalf(marshalErr, errors.CodeInternal, "marshal converted dashboard %s", storable.ID)
}
if err := json.Unmarshal(raw, new(DashboardSpec)); err != nil {
return nil, errors.WrapInvalidInputf(err, ErrCodeDashboardMigrationFailed, "converted dashboard %s is invalid", storable.ID)
}
tags := d.convertV1TagsForOrg(storable.OrgID, storable.Data["tags"])
if err := d.errIfHasMalformedFields(); err != nil {
return nil, err
}
return &DashboardV2{
Identifiable: storable.Identifiable,
TimeAuditable: storable.TimeAuditable,
UserAuditable: storable.UserAuditable,
OrgID: storable.OrgID,
Locked: storable.Locked,
Source: storable.Source,
DashboardV2MetadataBase: DashboardV2MetadataBase{
SchemaVersion: SchemaVersion,
Image: image,
},
Name: generateDashboardName(title),
Tags: tags,
Spec: spec,
}, nil
}
// V1Name derives the v2 name column from a v1 dashboard's title. Used to backfill the
// name of a dashboard whose data fails to migrate, so it stays findable by name.
func (storable StorableDashboard) V1Name() string {
return generateDashboardName(storable.Data.readString("title"))
}

View File

@@ -0,0 +1,214 @@
package dashboardtypes
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
)
// ══════════════════════════════════════════════
// v1 decoder
// ══════════════════════════════════════════════
// v1Decoder reads fields out of the untyped v1 dashboard blob. Every read*
// method follows the same contract: a field that is absent or null yields the
// zero value; a field present with the wrong type yields zero AND records a
// malformed-field error. Conversion proceeds (so one bad field doesn't abort
// the rest) and ConvertV1ToV2 returns d.malformedFieldsErr() at the end so the
// dashboard is logged and skipped.
//
// Polymorphic v1 fields (spanGaps bool|number, selectedValue string|array, …)
// are read with a type switch on the already-extracted value, never through
// these accessors, so they stay lenient by construction.
type v1Decoder struct {
bad []string
seen map[string]struct{}
}
// note records a decoding problem (malformed field, unknown value, swallowed
// sub-parse error), deduping identical messages. ConvertV1ToV2 surfaces these
// via errIfHasMalformedFields.
func (d *v1Decoder) note(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
if _, dup := d.seen[msg]; dup {
return
}
if d.seen == nil {
d.seen = make(map[string]struct{})
}
d.seen[msg] = struct{}{}
d.bad = append(d.bad, msg)
}
// noteMalformedField records a v1 field present with the wrong Go type.
func (d *v1Decoder) noteMalformedField(field string, raw any) {
d.note("%q has unexpected type %T", field, raw)
}
// detailErr renders an error for a diagnostic note, unfolding the structured
// detail our JSON binding attaches via WithAdditional. A plain %v on these
// errors prints only the innermost message ("request body contains invalid
// field value") and drops the field/type context that says which field was
// wrong — the part that actually tells you what to fix.
func detailErr(err error) string {
if err == nil {
return ""
}
j := errors.AsJSON(err)
if len(j.Errors) == 0 {
return err.Error()
}
details := make([]string, 0, len(j.Errors))
for _, e := range j.Errors {
details = append(details, e.Message)
}
return j.Message + ": " + strings.Join(details, "; ")
}
func (d *v1Decoder) errIfHasMalformedFields() error {
if len(d.bad) == 0 {
return nil
}
// One field per line: these lists run long (a bad widget query is reported
// once per widget), and a single "; "-joined line is an unscannable wall.
return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidData, "malformed v1 dashboard fields:\n %s", strings.Join(d.bad, "\n "))
}
func readField[T any](d *v1Decoder, m map[string]any, key string) T {
var zero T
v, present := m[key]
if !present || v == nil {
return zero
}
t, ok := v.(T)
if !ok {
d.noteMalformedField(key, v)
return zero
}
return t
}
func (d *v1Decoder) readString(m map[string]any, key string) string {
return readField[string](d, m, key)
}
func (d *v1Decoder) readFloat(m map[string]any, key string) float64 {
v, present := m[key]
if !present || v == nil {
return 0
}
f, ok := coerceFloat(v)
if !ok {
d.noteMalformedField(key, v)
return 0
}
return f
}
// coerceFloat accepts a JSON number or a numeric string (v1 sometimes stores
// numbers like softMin as quoted strings). A blank string is "unset", not a
// number, so it fails to coerce.
func coerceFloat(v any) (float64, bool) {
switch n := v.(type) {
case float64:
return n, true
case string:
f, err := strconv.ParseFloat(strings.TrimSpace(n), 64)
if err != nil {
return 0, false
}
return f, true
}
return 0, false
}
func (d *v1Decoder) readBool(m map[string]any, key string) bool { return readField[bool](d, m, key) }
func (d *v1Decoder) readArray(m map[string]any, key string) []any { return readField[[]any](d, m, key) }
func (d *v1Decoder) readObject(m map[string]any, key string) map[string]any {
return readField[map[string]any](d, m, key)
}
// readInt narrows a numeric field to int (JSON numbers decode as float64).
func (d *v1Decoder) readInt(m map[string]any, key string) int { return int(d.readFloat(m, key)) }
func (d *v1Decoder) readFloatPtr(m map[string]any, key string) *float64 {
v, present := m[key]
if !present || v == nil {
return nil
}
// A blank string means "unset" (v1's empty softMin/softMax), not malformed.
if s, ok := v.(string); ok && strings.TrimSpace(s) == "" {
return nil
}
f, ok := coerceFloat(v)
if !ok {
d.noteMalformedField(key, v)
return nil
}
return &f
}
// clipName truncates s to at most limit runes so a v1 name over a v2 length bound
// (MaxDisplayNameLen / MaxLayoutTitleLen) is shortened rather than failing migration.
func clipName(s string, limit int) string {
r := []rune(s)
if len(r) <= limit {
return s
}
return string(r[:limit])
}
func (d *v1Decoder) readStringMap(m map[string]any, key string) map[string]string {
// An empty list is a stand-in for an empty map here; tolerate it silently
// rather than flagging the wrong-type as malformed.
if s, ok := m[key].([]any); ok && len(s) == 0 {
return nil
}
raw := d.readObject(m, key)
if len(raw) == 0 {
return nil
}
out := make(map[string]string, len(raw))
for k, v := range raw {
s, ok := v.(string)
if !ok {
d.noteMalformedField(key+"."+k, v)
continue
}
out[k] = s
}
return out
}
func (d *v1Decoder) readObjects(m map[string]any, key string) []map[string]any {
raw := d.readArray(m, key)
if len(raw) == 0 {
return nil
}
out := make([]map[string]any, 0, len(raw))
for i, item := range raw {
obj, ok := item.(map[string]any)
if !ok {
d.noteMalformedField(fmt.Sprintf("%s[%d]", key, i), item)
continue
}
out = append(out, obj)
}
return out
}
// decodeMapInto converts an untyped map[string]any into a typed T by
// round-tripping through JSON, letting encoding/json (struct tags, custom
// UnmarshalJSON) do the field mapping instead of hand-copying out of the map.
func decodeMapInto[T any](src map[string]any) (T, error) {
var dst T
bytes, err := json.Marshal(src)
if err != nil {
return dst, err
}
if err := json.Unmarshal(bytes, &dst); err != nil {
return dst, err
}
return dst, nil
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,352 @@
package dashboardtypes
import (
"sort"
"strings"
"github.com/perses/spec/go/common"
"github.com/perses/spec/go/dashboard"
)
// panelRefPrefix is the JSON-ref prefix a grid item uses to point at a panel:
// "#/spec/panels/<id>".
const panelRefPrefix = "#/spec/panels/"
// sanitizePanelID rewrites a widget id to something valid in a panel $ref. Perses
// accepts only [a-zA-Z0-9_-] per ref segment (common.jsonRefMatching), so every
// other rune (em dash, spaces, dots, unicode, …) is mapped to a hyphen.
func sanitizePanelID(id string) string {
return strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', r == '-':
return r
default:
return '-'
}
}, id)
}
// sanitizeWidgetIDs rewrites every widget id in the raw v1 data — widgets[].id,
// layout[].i, panelMap keys and their widgets[].i — through sanitizePanelID, so a
// panel's map key and the layout $ref pointing at it stay identical (an illegal char
// in one but not the other would dangle the ref). Runs before panels/layouts build.
func sanitizeWidgetIDs(data StorableDashboardData) {
sanitizeField := func(raw any, field string) {
items, _ := raw.([]any)
for _, it := range items {
if m, ok := it.(map[string]any); ok {
if s, ok := m[field].(string); ok {
m[field] = sanitizePanelID(s)
}
}
}
}
sanitizeField(data["widgets"], "id")
sanitizeField(data["layout"], "i")
if panelMap, ok := data["panelMap"].(map[string]any); ok {
for key, v := range panelMap {
if s := sanitizePanelID(key); s != key {
panelMap[s] = v
delete(panelMap, key)
}
if m, ok := v.(map[string]any); ok {
sanitizeField(m["widgets"], "i")
}
}
}
}
// ══════════════════════════════════════════════
// Layouts (data.layout + data.panelMap)
// ══════════════════════════════════════════════
// convertV1Layouts groups v1 react-grid-layout entries into v2 grid layouts.
// Membership is positional (as the frontend renders): each row widget owns the
// panels below it until the next row; panels above the first row form an unnamed
// grid with no section header. Collapsed rows are the exception — their children
// live in panelMap[rowID].widgets, not `layout`.
func (d *v1Decoder) convertV1Layouts(data StorableDashboardData, panels map[string]*Panel) []Layout {
layout := d.readObjects(data, "layout")
if len(layout) == 0 {
return []Layout{}
}
// react-grid-layout can persist the same widget id more than once. Keep the first
// occurrence in stored order (mirroring getUpdatedLayout — the losing entry's
// geometry is discarded, not merged) and drop the rest. Dedupe before sortByPosition
// so "first" means first-in-stored-order, not topmost. Entries with no id are left
// for the main loop to drop.
seenWidgetIds := make(map[string]bool, len(layout))
dedupedLayouts := layout[:0]
for _, item := range layout {
if id := d.readString(item, "i"); id != "" {
if seenWidgetIds[id] {
continue
}
seenWidgetIds[id] = true
}
dedupedLayouts = append(dedupedLayouts, item)
}
layout = dedupedLayouts
rows := d.extractRowsAndCollapsedWidgets(data)
// ids placed directly in `layout`. A collapsed child also listed here is rendered from
// layout (the open section), so it's dropped from its collapsed section below.
placedInLayout := make(map[string]bool, len(layout))
for _, item := range layout {
if id := d.readString(item, "i"); id != "" {
placedInLayout[id] = true
}
}
d.sortByPosition(layout)
type section struct {
row *rowInfo // nil for the unnamed grid of ungrouped panels
items []map[string]any
}
topSectionWithoutHeader := &section{}
sectionsWithHeader := make([]*section, 0, len(rows))
currentRowHeader := topSectionWithoutHeader
for _, item := range layout {
id := d.readString(item, "i")
if id == "" {
continue
}
if row, ok := rows[id]; ok {
newRowHeader := &section{row: row, items: d.extractValidLayoutItemsForCollapsedSection(row.collapsedWidgets, panels, placedInLayout)}
sectionsWithHeader = append(sectionsWithHeader, newRowHeader)
// A collapsed row owns only its stashed children; later panels → ungrouped.
if row.collapsed {
currentRowHeader = topSectionWithoutHeader
} else {
currentRowHeader = newRowHeader
}
continue
}
// Keep a layout entry only if its widget became a panel; otherwise (skipped
// widget, deleted id, or the "__dropping-elem__" drag placeholder) it would
// reference a panel that does not exist. Rows are handled above.
if _, ok := panels[id]; !ok {
continue
}
currentRowHeader.items = append(currentRowHeader.items, item)
}
out := make([]Layout, 0, len(sectionsWithHeader)+1)
if len(topSectionWithoutHeader.items) > 0 {
out = append(out, d.buildV2GridLayout(nil, topSectionWithoutHeader.items))
}
for _, sec := range sectionsWithHeader {
out = append(out, d.buildV2GridLayout(sec.row, sec.items))
}
return out
}
// retainPlacedWidgets drops widgets the v1 layout never places, returning the
// filtered widgets. v1 doesn't render an unplaced widget, so converting it — and
// noting any problems it has — is pure noise; filter before conversion so only
// rendered widgets reach convertV1Panels. A non-array widgets value is returned
// untouched for convertV1Panels to flag; non-map entries are kept so it still
// flags them as malformed.
func retainPlacedWidgets(data StorableDashboardData) any {
widgets, ok := data["widgets"].([]any)
if !ok {
return data["widgets"]
}
placed := placedWidgetIDs(data)
kept := make([]any, 0, len(widgets))
for _, w := range widgets {
wm, ok := w.(map[string]any)
if !ok {
kept = append(kept, w) // malformed entry — leave it for convertV1Panels to note
continue
}
if id, _ := wm["id"].(string); placed[id] {
kept = append(kept, w)
}
}
return kept
}
// placedWidgetIDs returns the set of widget ids the v1 layout actually renders:
// every id in `layout`, plus the collapsed-row children stashed in panelMap.
// Read leniently (no malformed notes) — convertV1Layouts re-reads these and
// reports any genuine problems.
func placedWidgetIDs(data StorableDashboardData) map[string]bool {
ids := make(map[string]bool)
if layout, ok := data["layout"].([]any); ok {
for _, e := range layout {
if m, ok := e.(map[string]any); ok {
if i, ok := m["i"].(string); ok && i != "" {
ids[i] = true
}
}
}
}
if panelMap, ok := data["panelMap"].(map[string]any); ok {
for _, v := range panelMap {
m, ok := v.(map[string]any)
if !ok {
continue
}
widgets, ok := m["widgets"].([]any)
if !ok {
continue
}
for _, w := range widgets {
if wm, ok := w.(map[string]any); ok {
if i, ok := wm["i"].(string); ok && i != "" {
ids[i] = true
}
}
}
}
}
return ids
}
// extractValidLayoutItemsForCollapsedSection keeps only the collapsed-row children
// backed by a real panel and not already placed in `layout`, dropping ghosts and any
// child the open layout renders instead. These come from panelMap and skip the main
// loop's per-item panel check, so a grid never references a missing or twice-placed panel.
func (d *v1Decoder) extractValidLayoutItemsForCollapsedSection(items []map[string]any, panels map[string]*Panel, placedInLayout map[string]bool) []map[string]any {
out := make([]map[string]any, 0, len(items))
seen := make(map[string]bool, len(items))
for _, item := range items {
id := d.readString(item, "i")
if id == "" {
continue
}
if _, ok := panels[id]; !ok {
continue
}
if placedInLayout[id] || seen[id] {
continue
}
seen[id] = true
out = append(out, item)
}
return out
}
type rowInfo struct {
title string
collapsed bool
collapsedWidgets []map[string]any
}
// extractRowsAndCollapsedWidgets returns the row widgets keyed by id; collapsed
// rows also carry their children stashed under panelMap[id].widgets.
func (d *v1Decoder) extractRowsAndCollapsedWidgets(data StorableDashboardData) map[string]*rowInfo {
panelMap := d.readObject(data, "panelMap")
rows := make(map[string]*rowInfo)
for _, w := range d.readObjects(data, "widgets") {
// Read id directly (not via readString): a non-string id is skipped silently by
// convertV1Panels, so flagging it malformed here would fail the migration for a
// widget that's already been dropped.
id, _ := w["id"].(string)
if d.readString(w, "panelTypes") != "row" || id == "" {
continue
}
row := &rowInfo{title: d.readString(w, "title")}
// Some templates store panelMap[id] as a bare []widgetID instead of the
// canonical {widgets, collapsed}. The frontend treats such a non-object
// entry as "not collapsed" (see GridCardLayout), so read it leniently: a
// non-map yields nil, which reads as not collapsed.
pm, _ := panelMap[id].(map[string]any)
if d.readBool(pm, "collapsed") {
row.collapsed = true
row.collapsedWidgets = d.readObjects(pm, "widgets")
}
rows[id] = row
}
return rows
}
// buildV2GridLayout builds one v2 grid. row is nil for the unnamed grid (no
// display); otherwise the grid takes the row's title and collapse state. Items are
// sorted by (y, x) then vertically compacted (see compactGridItemsVertically).
func (d *v1Decoder) buildV2GridLayout(row *rowInfo, items []map[string]any) Layout {
d.sortByPosition(items)
spec := dashboard.GridLayoutSpec{Items: make([]dashboard.GridItem, 0, len(items))}
if row != nil {
spec.Display = &dashboard.GridLayoutDisplay{
Title: clipName(row.title, MaxLayoutTitleLen),
Collapse: &dashboard.GridLayoutCollapse{Open: !row.collapsed},
}
}
for _, item := range items {
id := d.readString(item, "i")
spec.Items = append(spec.Items, dashboard.GridItem{
X: d.readInt(item, "x"),
Y: d.readInt(item, "y"),
Width: d.readInt(item, "w"),
Height: d.readInt(item, "h"),
// Path mirrors what JSONRef.validate derives on decode (each "/segment"
// of the ref; the leading "#" captures nothing).
Content: &common.JSONRef{Ref: panelRefPrefix + id, Path: []string{"spec", "panels", id}},
})
}
compactGridItemsVertically(spec.Items)
return Layout{Kind: dashboard.KindGridLayout, Spec: &spec}
}
// compactGridItemsVertically mirrors react-grid-layout's correctBounds+compact
// (compactType "vertical", allowOverlap false): clamp each item into the grid (x,y>=0;
// x+width<=cols by shifting left), then move sorted-first items up to fill space and
// down past collisions. Fixes overlaps, gaps, and out-of-bounds coords so the migrated
// grid matches the v1 UI and passes v2 validation.
func compactGridItemsVertically(items []dashboard.GridItem) {
collides := func(a, b dashboard.GridItem) bool {
return a.X < b.X+b.Width && b.X < a.X+a.Width && a.Y < b.Y+b.Height && b.Y < a.Y+a.Height
}
firstCollision := func(l dashboard.GridItem, placed []dashboard.GridItem) (dashboard.GridItem, bool) {
for _, p := range placed {
if collides(l, p) {
return p, true
}
}
return dashboard.GridItem{}, false
}
for i := range items {
l := items[i]
if l.X+l.Width > gridColumnCount { // overflows right → shift left to fit
l.X = gridColumnCount - l.Width
}
if l.X < 0 {
l.X = 0
}
if l.Y < 0 {
l.Y = 0
}
for l.Y > 0 { // move up to fill space above
up := l
up.Y--
if _, hit := firstCollision(up, items[:i]); hit {
break
}
l.Y--
}
for { // then down past any collision with an already-placed item
c, hit := firstCollision(l, items[:i])
if !hit {
break
}
l.Y = c.Y + c.Height
}
items[i] = l
}
}
func (d *v1Decoder) sortByPosition(items []map[string]any) {
sort.SliceStable(items, func(i, j int) bool {
if yi, yj := d.readInt(items[i], "y"), d.readInt(items[j], "y"); yi != yj {
return yi < yj
}
return d.readInt(items[i], "x") < d.readInt(items[j], "x")
})
}

View File

@@ -0,0 +1,503 @@
package dashboardtypes
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// ══════════════════════════════════════════════
// Widgets → Panels
// ══════════════════════════════════════════════
// convertV1Panels walks the v1 `widgets` array and produces v2 panels keyed by
// the v1 widget id. WidgetRow entries (panelTypes == "row") are dropped here
// and consumed by convertV1Layouts as section headers.
func (d *v1Decoder) convertV1Panels(raw any) map[string]*Panel {
if raw == nil {
return map[string]*Panel{}
}
widgetsRaw, ok := raw.([]any)
if !ok {
d.noteMalformedField("widgets", raw)
return nil
}
panels := make(map[string]*Panel, len(widgetsRaw))
for i, widgetRaw := range widgetsRaw {
widget, ok := widgetRaw.(map[string]any)
if !ok {
d.noteMalformedField(fmt.Sprintf("widgets[%d]", i), widgetRaw)
continue
}
// A non-string (or missing) id can't be referenced by any layout entry, and
// v1 doesn't render such widgets either — skip silently, don't flag it as
// malformed. Read directly (not via readString) to avoid a malformed note.
id, ok := widget["id"].(string)
if !ok || id == "" {
continue
}
var panel *Panel
panelType := d.readString(widget, "panelTypes")
switch panelType {
case "graph":
panel = d.convertGraphWidget(widget)
case "time_series", "TIME_SERIES":
// Malformed panelTypes: the canonical v1 value is "graph". Some dashboards
// stored the v2/enum-style name instead; accept it as a time-series graph.
panel = d.convertGraphWidget(widget)
case "bar":
panel = d.convertBarWidget(widget)
case "value":
panel = d.convertValueWidget(widget)
case "pie":
panel = d.convertPieWidget(widget)
case "table":
panel = d.convertTableWidget(widget)
case "histogram":
panel = d.convertHistogramWidget(widget)
case "list":
panel = d.convertListWidget(widget)
case "row":
// "row" (section header) is handled by the layout pass;
continue
default:
// Unknown/unsupported panel type — v1 can't render it either, so skip the
// widget silently rather than failing the whole migration.
continue
}
if panel == nil {
continue
}
if len(panel.Spec.Queries) == 0 {
// No renderable queries — every query was dropped as unrenderable, or none
// were defined. v1 renders nothing, so skip the widget silently.
continue
}
// v1 has no panel links; emit [] (not nil) so the required links field
// round-trips, rather than setting it in every panel constructor.
panel.Spec.Links = []Link{}
panels[id] = panel
}
return panels
}
func (d *v1Decoder) convertGraphWidget(w map[string]any) *Panel {
return &Panel{
Kind: "Panel",
Spec: PanelSpec{
Display: d.widgetDisplay(w),
Plugin: PanelPlugin{
Kind: PanelKindTimeSeries,
Spec: &TimeSeriesPanelSpec{
Visualization: TimeSeriesVisualization{
BasicVisualization: d.basicVisualization(w),
FillSpans: d.readBool(w, "fillSpans"),
},
Formatting: d.panelFormatting(w),
ChartAppearance: TimeSeriesChartAppearance{
LineInterpolation: mapV1Enum(d.readString(w, "lineInterpolation"), LineInterpolationSpline,
LineInterpolationLinear, LineInterpolationSpline, LineInterpolationStepAfter, LineInterpolationStepBefore),
ShowPoints: d.readBool(w, "showPoints"),
LineStyle: mapV1Enum(d.readString(w, "lineStyle"), LineStyleSolid, LineStyleSolid, LineStyleDashed),
FillMode: mapV1Enum(d.readString(w, "fillMode"), FillModeNone, FillModeSolid, FillModeGradient, FillModeNone),
SpanGaps: mapV1SpanGaps(w["spanGaps"]),
},
Axes: d.axesFromWidget(w),
Legend: d.legendFromWidget(w),
Thresholds: d.mapV1ThresholdsWithLabel(w),
},
},
Queries: d.convertV1WidgetQuery(w, PanelKindTimeSeries),
},
}
}
func (d *v1Decoder) convertBarWidget(w map[string]any) *Panel {
return &Panel{
Kind: "Panel",
Spec: PanelSpec{
Display: d.widgetDisplay(w),
Plugin: PanelPlugin{
Kind: PanelKindBarChart,
Spec: &BarChartPanelSpec{
Visualization: BarChartVisualization{
BasicVisualization: d.basicVisualization(w),
FillSpans: d.readBool(w, "fillSpans"),
StackedBarChart: d.readBool(w, "stackedBarChart"),
},
Formatting: d.panelFormatting(w),
Axes: d.axesFromWidget(w),
Legend: d.legendFromWidget(w),
Thresholds: d.mapV1ThresholdsWithLabel(w),
},
},
Queries: d.convertV1WidgetQuery(w, PanelKindBarChart),
},
}
}
func (d *v1Decoder) convertValueWidget(w map[string]any) *Panel {
return &Panel{
Kind: "Panel",
Spec: PanelSpec{
Display: d.widgetDisplay(w),
Plugin: PanelPlugin{
Kind: PanelKindNumber,
Spec: &NumberPanelSpec{
Visualization: d.basicVisualization(w),
Formatting: d.panelFormatting(w),
Thresholds: d.mapV1ComparisonThresholds(w),
},
},
Queries: d.convertV1WidgetQuery(w, PanelKindNumber),
},
}
}
func (d *v1Decoder) convertPieWidget(w map[string]any) *Panel {
return &Panel{
Kind: "Panel",
Spec: PanelSpec{
Display: d.widgetDisplay(w),
Plugin: PanelPlugin{
Kind: PanelKindPieChart,
Spec: &PieChartPanelSpec{
Visualization: d.basicVisualization(w),
Formatting: d.panelFormatting(w),
Legend: d.legendFromWidget(w),
},
},
Queries: d.convertV1WidgetQuery(w, PanelKindPieChart),
},
}
}
func (d *v1Decoder) convertTableWidget(w map[string]any) *Panel {
return &Panel{
Kind: "Panel",
Spec: PanelSpec{
Display: d.widgetDisplay(w),
Plugin: PanelPlugin{
Kind: PanelKindTable,
Spec: &TablePanelSpec{
Visualization: d.basicVisualization(w),
Formatting: TableFormatting{
ColumnUnits: d.readStringMap(w, "columnUnits"),
DecimalPrecision: mapV1Precision(w["decimalPrecision"]),
},
Thresholds: d.mapV1TableThresholds(w),
},
},
Queries: d.convertV1WidgetQuery(w, PanelKindTable),
},
}
}
func (d *v1Decoder) convertHistogramWidget(w map[string]any) *Panel {
return &Panel{
Kind: "Panel",
Spec: PanelSpec{
Display: d.widgetDisplay(w),
Plugin: PanelPlugin{
Kind: PanelKindHistogram,
Spec: &HistogramPanelSpec{
HistogramBuckets: HistogramBuckets{
BucketCount: d.readFloatPtr(w, "bucketCount"),
BucketWidth: d.readFloatPtr(w, "bucketWidth"),
MergeAllActiveQueries: d.readBool(w, "mergeAllActiveQueries"),
},
Legend: d.legendFromWidget(w),
},
},
Queries: d.convertV1WidgetQuery(w, PanelKindHistogram),
},
}
}
func (d *v1Decoder) convertListWidget(w map[string]any) *Panel {
return &Panel{
Kind: "Panel",
Spec: PanelSpec{
Display: d.widgetDisplay(w),
Plugin: PanelPlugin{
Kind: PanelKindList,
Spec: &ListPanelSpec{
SelectFields: d.mapV1SelectFields(w),
},
},
Queries: d.convertV1WidgetQuery(w, PanelKindList),
},
}
}
// ══════════════════════════════════════════════
// Panel-spec shared helpers
// ══════════════════════════════════════════════
func (d *v1Decoder) widgetDisplay(w map[string]any) Display {
return Display{Name: clipName(d.readString(w, "title"), MaxDisplayNameLen), Description: d.readString(w, "description")}
}
func (d *v1Decoder) basicVisualization(w map[string]any) BasicVisualization {
return BasicVisualization{TimePreference: mapV1TimePreference(d.readString(w, "timePreferance"))}
}
func (d *v1Decoder) panelFormatting(w map[string]any) PanelFormatting {
return PanelFormatting{Unit: d.readString(w, "yAxisUnit"), DecimalPrecision: mapV1Precision(w["decimalPrecision"])}
}
func (d *v1Decoder) axesFromWidget(w map[string]any) Axes {
return Axes{
SoftMin: d.readFloatPtr(w, "softMin"),
SoftMax: d.readFloatPtr(w, "softMax"),
IsLogScale: d.readBool(w, "isLogScale"),
}
}
func (d *v1Decoder) legendFromWidget(w map[string]any) Legend {
return Legend{
Position: mapV1Enum(d.readString(w, "legendPosition"), LegendPositionBottom, LegendPositionBottom, LegendPositionRight),
CustomColors: d.readStringMap(w, "customLegendColors"),
}
}
// widgetBuilderSignal returns the signal of the widget's first builder query. Empty
// for non-builder (promql/clickhouse) widgets, which have no per-signal selected fields.
func (d *v1Decoder) widgetBuilderSignal(w map[string]any) telemetrytypes.Signal {
builder := d.readObject(d.readObject(w, "query"), "builder")
for _, q := range d.readObjects(builder, "queryData") {
return signalFromDataSource(q["dataSource"])
}
return telemetrytypes.Signal{}
}
func (d *v1Decoder) mapV1SelectFields(w map[string]any) []telemetrytypes.TelemetryFieldKey {
// Read the array matching the query's signal: traces and logs each keep their own
// selected columns, and a widget can carry a stale array for the other signal.
primary, fallback := "selectedLogFields", "selectedTracesFields"
if d.widgetBuilderSignal(w) == telemetrytypes.SignalTraces {
primary, fallback = "selectedTracesFields", "selectedLogFields"
}
field := primary
raw := d.readArray(w, field)
if len(raw) == 0 {
field = fallback
raw = d.readArray(w, field)
}
if len(raw) == 0 {
return nil
}
normalizePreV5FieldKeys(raw)
fields, err := decodeTelemetryFields(raw)
if err != nil {
d.note("widget %q has malformed %s: %v", d.readString(w, "id"), field, err)
return nil
}
// Drop nameless entries (blank column rows) — v2 requires a name, and the v1
// UI renders nothing for them anyway.
out := fields[:0]
for _, f := range fields {
if f.Name != "" {
out = append(out, f)
}
}
return out
}
func decodeTelemetryFields(raw []any) ([]telemetrytypes.TelemetryFieldKey, error) {
bytes, err := json.Marshal(raw)
if err != nil {
return nil, err
}
var fields []telemetrytypes.TelemetryFieldKey
if err := json.Unmarshal(bytes, &fields); err != nil {
return nil, err
}
return fields, nil
}
// ══════════════════════════════════════════════
// Panel field mappers
// ══════════════════════════════════════════════
// v1 stores timePreferance as `GLOBAL_TIME`, `LAST_5_MIN`, … (see
// frontend/src/container/NewWidget/RightContainer/timeItems.ts). v2 uses the
// lowercase form, so the translation is just downcase.
func mapV1TimePreference(s string) TimePreference {
if s == "" {
return TimePreferenceGlobalTime
}
candidate := TimePreference{valuer.NewString(strings.ToLower(s))}
for _, allowed := range candidate.Enum() {
if allowed == candidate {
return candidate
}
}
return TimePreferenceGlobalTime
}
// mapV1Precision is polymorphic (string|number), so it type-switches the raw
// value rather than reading through a typed accessor.
func mapV1Precision(raw any) PrecisionOption {
switch v := raw.(type) {
case string:
candidate := PrecisionOption{valuer.NewString(v)}
for _, allowed := range candidate.Enum() {
if allowed == candidate {
return candidate
}
}
case float64:
n := int(v)
if n >= 0 && n <= 4 {
return PrecisionOption{valuer.NewString(strconv.Itoa(n))}
}
}
return PrecisionOption2
}
// mapV1Enum picks the v1 string value if it matches one of the allowed v2
// values, otherwise returns the fallback. v1 frontend enums (lineInterpolation,
// lineStyle, fillMode, legendPosition) already use the v2 lowercase form.
func mapV1Enum[T interface{ StringValue() string }](s string, fallback T, allowed ...T) T {
if s == "" {
return fallback
}
for _, a := range allowed {
if a.StringValue() == s {
return a
}
}
return fallback
}
// v1 spanGaps is `boolean | number`. true → span every gap; false → never span;
// a number is interpreted (per frontend SeriesProps.spanGaps docs) as an
// X-axis threshold in seconds. Polymorphic, so it type-switches the raw value.
func mapV1SpanGaps(raw any) SpanGaps {
switch v := raw.(type) {
case bool:
return SpanGaps{FillOnlyBelow: false}
case float64:
return SpanGaps{FillOnlyBelow: true, FillLessThan: time.Duration(v * float64(time.Second)).String()}
}
return SpanGaps{FillOnlyBelow: false}
}
func (d *v1Decoder) mapV1ThresholdsWithLabel(w map[string]any) []ThresholdWithLabel {
rawSlice := d.readObjects(w, "thresholds")
if len(rawSlice) == 0 {
return nil
}
out := make([]ThresholdWithLabel, 0, len(rawSlice))
for _, t := range rawSlice {
color := d.readString(t, "thresholdColor")
if color == "" {
// Only color is required on ThresholdWithLabel; label is optional, so an
// unlabeled threshold must survive.
continue
}
value := d.readFloat(t, "thresholdValue")
out = append(out, ThresholdWithLabel{Value: value, Unit: d.readString(t, "thresholdUnit"), Color: color, Label: d.readString(t, "thresholdLabel")})
}
if len(out) == 0 {
return nil
}
return out
}
func (d *v1Decoder) mapV1ComparisonThresholds(w map[string]any) []ComparisonThreshold {
rawSlice := d.readObjects(w, "thresholds")
if len(rawSlice) == 0 {
return nil
}
out := make([]ComparisonThreshold, 0, len(rawSlice))
for _, t := range rawSlice {
color := d.readString(t, "thresholdColor")
if color == "" {
continue
}
value := d.readFloat(t, "thresholdValue")
out = append(out, ComparisonThreshold{
Value: value,
Operator: d.mapV1ComparisonOperator(d.readString(t, "thresholdOperator")),
Unit: d.readString(t, "thresholdUnit"),
Color: color,
Format: mapV1ThresholdFormat(t["thresholdFormat"]),
})
}
if len(out) == 0 {
return nil
}
return out
}
func (d *v1Decoder) mapV1TableThresholds(w map[string]any) []TableThreshold {
rawSlice := d.readObjects(w, "thresholds")
if len(rawSlice) == 0 {
return nil
}
out := make([]TableThreshold, 0, len(rawSlice))
for _, t := range rawSlice {
color := d.readString(t, "thresholdColor")
columnName := d.readString(t, "thresholdTableOptions")
if color == "" || columnName == "" {
continue
}
value := d.readFloat(t, "thresholdValue")
out = append(out, TableThreshold{
ComparisonThreshold: ComparisonThreshold{
Value: value,
Operator: d.mapV1ComparisonOperator(d.readString(t, "thresholdOperator")),
Unit: d.readString(t, "thresholdUnit"),
Color: color,
Format: mapV1ThresholdFormat(t["thresholdFormat"]),
},
ColumnName: columnName,
})
}
if len(out) == 0 {
return nil
}
return out
}
func (d *v1Decoder) mapV1ComparisonOperator(s string) ComparisonOperator {
switch s {
case ">", "gt":
return ComparisonOperatorAbove
case ">=", "gte":
return ComparisonOperatorAboveOrEqual
case "<", "lt":
return ComparisonOperatorBelow
case "<=", "lte":
return ComparisonOperatorBelowOrEqual
case "=", "==", "eq":
return ComparisonOperatorEqual
case "!=", "neq":
return ComparisonOperatorNotEqual
default:
// v1 often leaves the operator empty or carries an unknown value; default to
// "above" without flagging.
return ComparisonOperatorAbove
}
}
// mapV1ThresholdFormat reads the raw value (not via readString) so a non-string
// thresholdFormat — some v1 dashboards store it as a number — defaults to text
// silently instead of being flagged malformed.
func mapV1ThresholdFormat(raw any) ThresholdFormat {
s, _ := raw.(string)
switch strings.ToLower(s) {
case "background":
return ThresholdFormatBackground
case "text":
return ThresholdFormatText
}
return ThresholdFormatText
}

View File

@@ -0,0 +1,448 @@
package dashboardtypes
import (
"encoding/json"
"strconv"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
// ══════════════════════════════════════════════
// Queries
// ══════════════════════════════════════════════
// convertV1WidgetQuery returns exactly one Query (per Spec.Validate). The kind
// chosen depends on the v1 widget query shape:
// - a single query (promql / clickhouse_sql / builder) → its native kind
// - multiple queries → signoz/CompositeQuery
//
// A single query is never wrapped in a CompositeQuery; in particular List
// panels accept only a bare signoz/BuilderQuery. Builder queries are routed
// through qb.WrapInV5Envelope (in collectV1QueryEnvelopes), which translates v4
// builder-field names (orderBy/selectColumns/dataSource) into their v5
// equivalents and adds the `signal` field required by BuilderQuerySpec's
// per-signal dispatch.
func (d *v1Decoder) convertV1WidgetQuery(widget map[string]any, panelKind PanelPluginKind) []Query {
envelopes, signal := d.collectV1QueryEnvelopes(widget, panelKind)
if len(envelopes) == 0 {
return nil
}
// List panels accept only a bare BuilderQuery — never a CompositeQuery. Keep the
// first query and drop the rest so a multi-query v1 list widget still migrates.
if panelKind == PanelKindList && len(envelopes) > 1 {
envelopes = envelopes[:1]
}
requestType := requestTypeForPanel(panelKind)
// A single query keeps its native kind — never wrapped in a CompositeQuery.
if len(envelopes) == 1 {
if q := singleQueryFromEnvelope(envelopes[0], requestType, signal); q != nil {
return []Query{*q}
}
}
// Default: wrap in CompositeQuery.
composite, err := parseCompositeFromEnvelopes(envelopes)
if err != nil || composite == nil {
d.note("widget %q: could not build query from %d envelope(s): %s", d.readString(widget, "id"), len(envelopes), detailErr(err))
return nil
}
return []Query{{
Kind: requestType,
Spec: QuerySpec{
Plugin: QueryPlugin{Kind: QueryKindComposite, Spec: composite},
},
}}
}
// dropUnrenderableQueries removes queries whose aggregation can't render (see
// queryIsUnrenderable). If every query is unrenderable the result is empty, so the
// widget produces no panel and is skipped silently (convertV1Panels) — matching v1,
// which renders nothing.
func dropUnrenderableQueries(queries []map[string]any) []map[string]any {
renderable := make([]map[string]any, 0, len(queries))
for _, q := range queries {
if !queryIsUnrenderable(q) {
renderable = append(renderable, q)
}
}
return renderable
}
// queryIsUnrenderable reports whether a builder query can't render because of its
// aggregations: a metrics query with none or an empty metric name, or a logs/traces
// query with an empty aggregation expression. No aggregations is valid for a raw
// logs/traces query, so that isn't flagged.
func queryIsUnrenderable(q map[string]any) bool {
aggs, _ := q["aggregations"].([]any)
switch signalFromDataSource(q["dataSource"]) {
case telemetrytypes.SignalMetrics:
if len(aggs) == 0 {
return true
}
for _, a := range aggs {
if agg, ok := a.(map[string]any); ok {
if mn, _ := agg["metricName"].(string); mn == "" {
return true
}
}
}
case telemetrytypes.SignalLogs, telemetrytypes.SignalTraces:
for _, a := range aggs {
if agg, ok := a.(map[string]any); ok {
if expr, _ := agg["expression"].(string); expr == "" {
return true
}
}
}
}
return false
}
// requestTypeForPanel maps a v2 panel plugin kind to the request type (result
// shape) its queries produce. Mirrors the frontend's panelTypeToRequestType
// (buildQueryRangeRequest.ts): time series for line/bar/histogram (histogram
// bins client-side from raw time series, V1 parity), scalar for
// number/pie/table, raw rows for list.
func requestTypeForPanel(panelKind PanelPluginKind) qb.RequestType {
switch panelKind {
case PanelKindTimeSeries, PanelKindBarChart, PanelKindHistogram:
return qb.RequestTypeTimeSeries
case PanelKindNumber, PanelKindPieChart, PanelKindTable:
return qb.RequestTypeScalar
case PanelKindList:
return qb.RequestTypeRaw
}
return qb.RequestTypeTimeSeries
}
// collectV1QueryEnvelopes inspects widget.query.queryType and produces a
// flattened list of v5-shaped envelopes. The returned signal is the dominant
// builder signal (if any), used for typed builder-query dispatch.
func (d *v1Decoder) collectV1QueryEnvelopes(widget map[string]any, panelKind PanelPluginKind) ([]map[string]any, telemetrytypes.Signal) {
queryMap := d.readObject(widget, "query")
if queryMap == nil {
d.note("widget %q has no query map", d.readString(widget, "id"))
return nil, telemetrytypes.Signal{}
}
rowLimitPanel := panelKind == PanelKindList || panelKind == PanelKindTable
// Raw (list) panels legitimately have no aggregation; every other panel needs one.
needsAggregation := requestTypeForPanel(panelKind) != qb.RequestTypeRaw
queryType := d.readString(queryMap, "queryType")
switch queryType {
case "promql":
promQueries := d.readObjects(queryMap, "promql")
var out []map[string]any
for _, q := range promQueries {
// Drop empty queries; if none remain the widget produces no queries and is
// skipped silently (convertV1Panels), as v1 renders nothing.
if d.readString(q, "query") == "" {
continue
}
out = append(out, promQLEnvelope(q))
}
return out, telemetrytypes.Signal{}
case "clickhouse_sql":
chQueries := d.readObjects(queryMap, "clickhouse_sql")
var out []map[string]any
for _, q := range chQueries {
// Drop empty queries; if none remain the widget produces no queries and is
// skipped silently (convertV1Panels), as v1 renders nothing.
if d.readString(q, "query") == "" {
continue
}
out = append(out, clickhouseEnvelope(q))
}
return out, telemetrytypes.Signal{}
// "builder" plus a blank queryType: v1 defaults an unset type to builder, so a
// missing value still carries a real builder query — try the builder path.
case "builder", "":
builder := d.readObject(queryMap, "builder")
if builder == nil {
d.note("widget %q has no builder data in the query map", d.readString(widget, "id"))
return nil, telemetrytypes.Signal{}
}
var out []map[string]any
var signal telemetrytypes.Signal
widgetType := d.readString(widget, "panelTypes")
queries := d.readObjects(builder, "queryData")
assignQueryDataNames(queries)
for _, q := range queries {
normalizePreV5QueryData(q, widgetType)
normalizePreV5SelectColumns(q)
normalizePreV5GroupBy(q)
normalizePreV5PageSize(q, rowLimitPanel)
normalizeQueryLimit(q)
if needsAggregation {
ensureDefaultAggregation(q)
}
// After the aggregation is settled (incl. an injected default), so a
// value-order key (#SIGNOZ_VALUE) can resolve against it.
normalizeOrderByKeys(q)
}
queries = dropUnrenderableQueries(queries)
for _, q := range queries {
name := d.readString(q, "queryName")
out = append(out, qb.WrapInV5Envelope(name, q, string(qb.QueryTypeBuilder.StringValue())))
if signal.IsZero() {
signal = signalFromDataSource(q["dataSource"])
}
}
formulas := d.readObjects(builder, "queryFormulas")
assignMissingFormulaNames(formulas)
for _, f := range formulas {
normalizePreV5QueryData(f, widgetType)
name := d.readString(f, "queryName")
env := qb.WrapInV5Envelope(name, f, string(qb.QueryTypeFormula.StringValue()))
backfillFormulaFields(env, f)
// Drop a formula whose expression the validator rejects (blank/unparseable);
// v1 tolerated it but v2 fails the whole query. Reuse the real validator
// rather than reimplement it, as we do for functions.
if !formulaEnvelopeIsValid(env) {
continue
}
out = append(out, env)
}
for _, op := range d.readObjects(builder, "queryTraceOperator") {
// A trace operator's expression is the operation itself ("A=>B->C") and is
// required (ParseExpression rejects a blank one); drop it if empty.
expression := d.readString(op, "expression")
if expression == "" {
continue
}
normalizePreV5QueryData(op, widgetType)
normalizePreV5GroupBy(op)
normalizeOrderByKeys(op)
name := d.readString(op, "queryName")
out = append(out, traceOperatorEnvelope(name, expression, op))
}
return out, signal
default:
d.note("widget %q has unknown queryType %q", d.readString(widget, "id"), queryType)
}
return nil, telemetrytypes.Signal{}
}
// traceOperatorEnvelope builds a v5 builder_trace_operator envelope. WrapInV5Envelope
// would misclassify a trace operator as a formula (its name differs from its
// expression, e.g. "A=>B->C"), so route the map through the builder-query path —
// temporarily aligning expression with name to dodge that heuristic — then restore the
// real expression, drop the builder-only signal (a trace operator's spec has no signal
// field), and set the trace-operator type.
func traceOperatorEnvelope(name, expression string, op map[string]any) map[string]any {
op["expression"] = name
env := qb.WrapInV5Envelope(name, op, string(qb.QueryTypeBuilder.StringValue()))
if spec, ok := env["spec"].(map[string]any); ok {
delete(spec, "signal")
spec["expression"] = expression
}
env["type"] = string(qb.QueryTypeTraceOperator.StringValue())
return env
}
// maxQueries mirrors the frontend MAX_QUERIES; builder query names run A..Z.
const maxQueries = 26
// assignQueryDataNames names builder data queries the way the frontend does: each
// unnamed query takes the first unused A..Z, deduped against existing names. It also
// forces expression == queryName, since a data query's expression is always its own
// name and WrapInV5Envelope's name != expression heuristic would otherwise
// misclassify the query as a formula.
func assignQueryDataNames(queries []map[string]any) {
taken := make(map[string]bool, len(queries))
for _, q := range queries {
if name, _ := q["queryName"].(string); name != "" {
taken[name] = true
}
}
for _, q := range queries {
name, _ := q["queryName"].(string)
if name == "" {
for i := 0; i < maxQueries; i++ {
candidate := string(rune('A' + i))
if !taken[candidate] {
name = candidate
taken[candidate] = true
break
}
}
q["queryName"] = name
}
q["expression"] = name
}
}
// formulaEnvelopeIsValid reports whether a builder_formula envelope's spec passes
// QueryBuilderFormula.Validate (blank/unparseable expression, blank name, invalid
// functions). Reuses the real validator rather than reimplementing it.
func formulaEnvelopeIsValid(env map[string]any) bool {
spec, ok := env["spec"].(map[string]any)
if !ok {
return false
}
raw, err := json.Marshal(spec)
if err != nil {
return false
}
var f qb.QueryBuilderFormula
if err := json.Unmarshal(raw, &f); err != nil {
return false
}
return f.Validate() == nil
}
// maxFormulas mirrors the frontend MAX_FORMULAS; formula names run F1..F20.
const maxFormulas = 20
// assignMissingFormulaNames fills queryName for unnamed formulas, mirroring the
// frontend: pick the first F{n} (n in 1..20) not already used by another formula.
// Formulas that already have a name keep it.
func assignMissingFormulaNames(formulas []map[string]any) {
taken := make(map[string]bool, len(formulas))
for _, f := range formulas {
if name, _ := f["queryName"].(string); name != "" {
taken[name] = true
}
}
for _, f := range formulas {
if name, _ := f["queryName"].(string); name != "" {
continue
}
for i := 1; i <= maxFormulas; i++ {
candidate := "F" + strconv.Itoa(i)
if !taken[candidate] {
f["queryName"] = candidate
taken[candidate] = true
break
}
}
}
}
func promQLEnvelope(q map[string]any) map[string]any {
return map[string]any{
"type": qb.QueryTypePromQL.StringValue(),
"spec": map[string]any{
"name": q["name"],
"query": q["query"],
"disabled": q["disabled"],
"legend": q["legend"],
},
}
}
func clickhouseEnvelope(q map[string]any) map[string]any {
return map[string]any{
"type": qb.QueryTypeClickHouseSQL.StringValue(),
"spec": map[string]any{
"name": q["name"],
"query": q["query"],
"disabled": q["disabled"],
"legend": q["legend"],
},
}
}
// singleQueryFromEnvelope returns a typed Query for one envelope, using its
// native query kind (promql/clickhouse_sql/builder) rather than wrapping it in
// a CompositeQuery. A bare signoz/BuilderQuery is valid for every panel kind
// and is the only kind List panels accept.
func singleQueryFromEnvelope(envelope map[string]any, requestType qb.RequestType, signal telemetrytypes.Signal) *Query {
t, _ := envelope["type"].(string)
spec, _ := envelope["spec"].(map[string]any)
switch t {
case qb.QueryTypePromQL.StringValue():
prom, err := decodeMapInto[qb.PromQuery](spec)
if err != nil {
return nil
}
return &Query{
Kind: requestType,
Spec: QuerySpec{
Name: prom.Name,
Plugin: QueryPlugin{Kind: QueryKindPromQL, Spec: &prom},
},
}
case qb.QueryTypeClickHouseSQL.StringValue():
ch, err := decodeMapInto[qb.ClickHouseQuery](spec)
if err != nil {
return nil
}
return &Query{
Kind: requestType,
Spec: QuerySpec{
Name: ch.Name,
Plugin: QueryPlugin{Kind: QueryKindClickHouseSQL, Spec: &ch},
},
}
case qb.QueryTypeBuilder.StringValue():
builderSpec := parseBuilderQuerySpec(spec, signal)
if builderSpec == nil {
return nil
}
name, _ := spec["name"].(string)
return &Query{
Kind: requestType,
Spec: QuerySpec{
Name: name,
Plugin: QueryPlugin{Kind: QueryKindBuilder, Spec: &BuilderQuerySpec{Spec: builderSpec}},
},
}
}
return nil
}
func parseCompositeFromEnvelopes(envelopes []map[string]any) (*CompositeQuerySpec, error) {
bytes, err := json.Marshal(envelopes)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "marshal v1 query envelopes")
}
var parsed []qb.QueryEnvelope
if err := json.Unmarshal(bytes, &parsed); err != nil {
return nil, errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidWidgetQuery, "decode v5 query envelopes")
}
return &CompositeQuerySpec{Queries: parsed}, nil
}
func parseBuilderQuerySpec(rawSpec any, signal telemetrytypes.Signal) any {
spec, ok := rawSpec.(map[string]any)
if !ok {
return nil
}
if !signal.IsZero() {
spec["signal"] = signal.StringValue()
}
bytes, err := json.Marshal(spec)
if err != nil {
return nil
}
parsed, err := qb.UnmarshalBuilderQueryBySignal(bytes)
if err != nil {
return nil
}
return parsed
}
// signalFromDataSource maps a v1 data-source string to a v5 signal. Casing
// varies by source: builder queries store lowercase ("traces"), while variable
// `dynamicVariablesSource` stores capitalized ("Traces"), so match
// case-insensitively. Unknown values (e.g. "All telemetry") map to the zero
// Signal.
func signalFromDataSource(raw any) telemetrytypes.Signal {
s, _ := raw.(string)
switch strings.ToLower(s) {
case "traces":
return telemetrytypes.SignalTraces
case "logs":
return telemetrytypes.SignalLogs
case "metrics":
return telemetrytypes.SignalMetrics
}
return telemetrytypes.Signal{}
}

View File

@@ -0,0 +1,526 @@
package dashboardtypes
import (
"context"
"encoding/json"
"log/slog"
"regexp"
"strings"
"github.com/SigNoz/signoz/pkg/transition"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// ══════════════════════════════════════════════
// Malformed-field normalization
// ══════════════════════════════════════════════
//
// Pre-v5 query-body reshapes for dashboards whose bodies aren't actually v5-shaped
// (e.g. stamped version:"v5" but never upgraded). The bulk of the upgrade is
// delegated to transition.MigrateQueryDataShapeSafe (see normalizePreV5QueryData);
// this file keeps only the reshapes it doesn't cover.
// preV5Migrator runs transition's shape-safe (idempotent) v4→v5 upgrade. Stateless
// after construction, so a shared instance with a discard logger / no ambiguity
// keys is fine.
var preV5Migrator = transition.NewDashboardMigrateV5(slog.New(slog.DiscardHandler), nil, nil)
// normalizePreV5QueryData upgrades one builder queryData/formula in place: the
// shared migrator, then a reshape of any existing aggregations[] it leaves alone.
func normalizePreV5QueryData(query map[string]any, widgetType string) {
dropLegacyFilter(query)
normalizeFilterItemOps(query)
foldReduceToIntoMetricAggregations(query)
preV5Migrator.MigrateQueryDataShapeSafe(context.Background(), query, widgetType)
normalizePreV5LogTraceAggregations(query)
normalizeMetricAggregations(query)
normalizeFunctionArgs(query)
dropInvalidFunctions(query)
// normalizeOrderByKeys runs in the caller, after ensureDefaultAggregation: a
// value-order key resolves against the aggregation that may have just been injected.
}
// dropInvalidFunctions removes any function the v5 validator would reject — an unknown
// name, or a missing/uncastable required arg (see Function.Validate). v1 tolerated these
// but v2 fails the whole query, so we drop just the offending function. Runs after
// normalizeFunctionArgs so a merely double-wrapped (but otherwise valid) function isn't
// lost.
func dropInvalidFunctions(query map[string]any) {
fns, ok := query["functions"].([]any)
if !ok {
return
}
kept := make([]any, 0, len(fns))
for _, f := range fns {
raw, err := json.Marshal(f)
if err != nil {
continue
}
var fn qb.Function
if err := json.Unmarshal(raw, &fn); err != nil {
continue
}
if fn.Validate() != nil {
continue
}
kept = append(kept, f)
}
query["functions"] = kept
}
// normalizeFunctionArgs collapses a doubly-wrapped function arg to a scalar. The
// v4→v5 migration that runs before ConvertV1ToV2 (transition.updateQueryData) wraps every arg as
// {name, value} without checking whether it's already a v5 arg, so a body that was
// already v5 comes back as {value:{value:60}} and fails validation ("must be a floating
// value"). We can't guard it at the source — transition's Migrate is shared and left
// untouched — so unwrap one level of {value:...} nesting here.
func normalizeFunctionArgs(query map[string]any) {
fns, ok := query["functions"].([]any)
if !ok {
return
}
for _, f := range fns {
fn, ok := f.(map[string]any)
if !ok {
continue
}
args, ok := fn["args"].([]any)
if !ok {
continue
}
for _, a := range args {
arg, ok := a.(map[string]any)
if !ok {
continue
}
if inner, ok := arg["value"].(map[string]any); ok {
if v, ok := inner["value"]; ok {
arg["value"] = v
}
}
}
}
}
// malformedOrderByValueKeys are v4 order-by columnNames meaning "order by the aggregation value"
// that the v5 aggregation validator rejects (validateOrderByForAggregation). All resolve
// to the same aggregation key. Add more as they surface. The frontend passes these
// through (the query-service resolves them), but the v2 dashboard validator only accepts
// a real aggregation key.
var malformedOrderByValueKeys = map[string]bool{
"#SIGNOZ_VALUE": true,
"A": true,
"A.count()": true,
"__result": true,
"value": true,
"A.p99(duration_nano)": true,
"aws_Kafka_MessagesInPerSec_max": true,
"byte_in_count": true,
"(http_server_request_duration_ms.bucket)": true,
}
// normalizeOrderByKeys rewrites any orderBy columnName in orderByValueKeys to the
// v5-valid aggregation key. Left untouched if the key can't resolve (no aggregation to
// name).
func normalizeOrderByKeys(query map[string]any) {
orders, ok := query["orderBy"].([]any)
if !ok {
return
}
key, ok := aggregationOrderKey(query)
if !ok {
return
}
for _, o := range orders {
order, ok := o.(map[string]any)
if !ok {
continue
}
if cn, _ := order["columnName"].(string); malformedOrderByValueKeys[cn] {
order["columnName"] = key
}
}
}
// aggregationOrderKey names the first aggregation the way validateOrderByForAggregation
// expects: "space(metricName)" for metrics, the expression for logs/traces.
func aggregationOrderKey(query map[string]any) (string, bool) {
aggs, ok := query["aggregations"].([]any)
if !ok || len(aggs) == 0 {
return "", false
}
agg, ok := aggs[0].(map[string]any)
if !ok {
return "", false
}
if signalFromDataSource(query["dataSource"]) == telemetrytypes.SignalMetrics {
metricName, _ := agg["metricName"].(string)
space, _ := agg["spaceAggregation"].(string)
if metricName == "" || space == "" {
return "", false
}
return space + "(" + metricName + ")", true
}
expr, _ := agg["expression"].(string)
if expr == "" {
return "", false
}
return expr, true
}
// backfillFormulaFields restores order/limit/having onto the formula's spec.
// WrapInV5Envelope's formula branch emits only name/expression/disabled/legend/functions
// and drops these three, even though QueryBuilderFormula supports them.
func backfillFormulaFields(env, formula map[string]any) {
spec := env["spec"].(map[string]any)
// limit and having are already v5-shaped (the shape-safe migrator rewrites having),
// so copy them across unchanged.
if limit, ok := formula["limit"]; ok {
spec["limit"] = limit
}
if having, ok := formula["having"]; ok {
spec["having"] = having
}
// orderBy is still in the v4 shape ([{columnName, order}]); reshape each entry into
// the v5 order shape ([{key: {name}, direction}]).
orderBy, ok := formula["orderBy"].([]any)
if !ok {
return
}
order := make([]any, 0, len(orderBy))
for _, item := range orderBy {
ob, ok := item.(map[string]any)
if !ok {
continue
}
order = append(order, map[string]any{
"key": map[string]any{"name": ob["columnName"]},
"direction": ob["order"],
})
}
spec["order"] = order
}
// dropLegacyFilter removes a v4-shaped filter ({items, op}) stored under the v5
// `filter` key. The v5 filter is {expression}; the migrator only rewrites the v4
// `filters` key and skips when `filter` is present, so this stale shape would reach
// WrapInV5Envelope and fail v5 validation. The v1 UI ignores it — it types
// IBuilderQuery.filter as {expression} (frontend queryBuilderData.ts, filter?: Filter)
// and only ever reads filter.expression, so items/op go unread. We drop it before the
// migrator, which can then rebuild `filter` from `filters` if present.
func dropLegacyFilter(query map[string]any) {
filter, ok := query["filter"].(map[string]any)
if !ok {
return
}
_, hasItems := filter["items"]
_, hasOp := filter["op"]
if hasItems || hasOp {
delete(query, "filter")
}
}
// normalizeFilterItemOps lowercases exists/nexists filter ops (frontend stores them
// uppercase) to the spelling transition's buildCondition (pkg/transition/migrate_common.go)
// matches; otherwise it appends a spurious empty value ("svc EXISTS ”"). Value
// operators already round-trip via that switch's default case.
func normalizeFilterItemOps(query map[string]any) {
filters, ok := query["filters"].(map[string]any)
if !ok {
return
}
items, ok := filters["items"].([]any)
if !ok {
return
}
for _, it := range items {
item, ok := it.(map[string]any)
if !ok {
continue
}
op, ok := item["op"].(string)
if !ok {
continue
}
switch strings.ToLower(strings.ReplaceAll(op, "_", " ")) {
case "exists":
item["op"] = "exists"
case "nexists", "not exists":
item["op"] = "nexists"
}
}
}
// metricAggregationFields are the JSON keys a metric aggregation accepts (see
// MetricAggregation). The decoder is strict, so any other key (e.g. a logs/traces
// style `expression`) is rejected as an unknown field.
var metricAggregationFields = map[string]bool{
"metricName": true,
"temporality": true,
"timeAggregation": true,
"spaceAggregation": true,
"comparisonSpaceAggregationParam": true,
"reduceTo": true,
}
// normalizeMetricAggregations reshapes a metric query's aggregations to the shape v5
// expects. v1 bodies sometimes carry a logs/traces-style aggregation ({expression});
// the frontend ignores expression for metrics and builds from the metric fields
// (createAggregation, prepareQueryRangePayloadV5.ts), so we drop every non-metric
// key. A dropped expression leaves metricName empty and the widget is skipped later
// (isUnrenderableMetricQuery), matching what v1 renders.
//
// It also defaults an invalid spaceAggregation to "sum": v1 bodies often leave it
// empty or carry a stale value, which fails validation (SpaceAggregation.IsValid). A
// valid value (including a histogram percentile) is left alone; the metric type isn't
// in the body, so we can't prefer a percentile default for histograms.
func normalizeMetricAggregations(query map[string]any) {
if signalFromDataSource(query["dataSource"]) != telemetrytypes.SignalMetrics {
return
}
aggs, ok := query["aggregations"].([]any)
if !ok {
return
}
for _, a := range aggs {
agg, ok := a.(map[string]any)
if !ok {
continue
}
for k := range agg {
if !metricAggregationFields[k] {
delete(agg, k)
}
}
sa, _ := agg["spaceAggregation"].(string)
if !(metrictypes.SpaceAggregation{String: valuer.NewString(sa)}).IsValid() {
agg["spaceAggregation"] = metrictypes.SpaceAggregationSum.StringValue()
}
}
}
// foldReduceToIntoMetricAggregations moves a metric query's top-level reduceTo onto
// its existing aggregations[] (where v5 wants it), which the shared migrator only does
// when building an aggregation from flat fields. Runs before it so the value survives.
func foldReduceToIntoMetricAggregations(query map[string]any) {
if signalFromDataSource(query["dataSource"]) != telemetrytypes.SignalMetrics {
return
}
reduceTo, ok := query["reduceTo"].(string)
if !ok || reduceTo == "" {
return
}
aggs, ok := query["aggregations"].([]any)
if !ok || len(aggs) == 0 {
return
}
for _, a := range aggs {
agg, ok := a.(map[string]any)
if !ok {
continue
}
if _, exists := agg["reduceTo"]; !exists {
agg["reduceTo"] = reduceTo
}
}
}
// normalizePreV5LogTraceAggregations reshapes an existing logs/traces aggregations[]
// via parseAggregations (extract func(args), lift inline "as alias", split
// multi-part, drop metric-only fields; empty → count()). Covers the case the
// migrator skips: it builds from flat fields but leaves a present-but-malformed
// aggregations[] alone. A query with none is left as-is.
func normalizePreV5LogTraceAggregations(query map[string]any) {
switch signalFromDataSource(query["dataSource"]) {
case telemetrytypes.SignalLogs, telemetrytypes.SignalTraces:
default:
return
}
aggs, ok := query["aggregations"].([]any)
if !ok || len(aggs) == 0 {
return
}
out := make([]any, 0, len(aggs))
for _, a := range aggs {
agg, ok := a.(map[string]any)
if !ok {
continue
}
expr, _ := agg["expression"].(string)
alias, _ := agg["alias"].(string)
parsed := parseAggregations(expr, alias)
if len(parsed) == 0 {
parsed = []any{map[string]any{"expression": "count()"}}
}
out = append(out, parsed...)
}
query["aggregations"] = out
}
// ensureDefaultAggregation defaults an empty logs/traces aggregations[] to count(),
// mirroring the frontend. Callers gate this to aggregation panels. Metrics are skipped:
// count() can't stand in for a missing metricName.
func ensureDefaultAggregation(query map[string]any) {
switch signalFromDataSource(query["dataSource"]) {
case telemetrytypes.SignalLogs, telemetrytypes.SignalTraces:
default:
return
}
if aggs, ok := query["aggregations"].([]any); ok && len(aggs) > 0 {
return
}
query["aggregations"] = []any{map[string]any{"expression": "count()"}}
}
// aggExprRe matches one "func(args)" with an optional "as alias". Mirrors the
// frontend's parseAggregations regex; matching only well-formed func(args)
// discards trailing junk ("sum(x) ) )" → "sum(x)").
var aggExprRe = regexp.MustCompile(`([a-zA-Z0-9_]+\([^)]*\))(?:\s*as\s+('[^']*'|"[^"]*"|[a-zA-Z0-9_-]+))?`)
// aggExprNestedRe is a backup for aggExprRe that tolerates one level of nested
// parens in args (rate(count())). HACK: the flat aggExprRe (and the frontend it
// mirrors) truncates such exprs to an unbalanced "rate(count()"; the UI fails
// these today, so this is best-effort beyond v1. Tried only when the flat match
// comes back unbalanced.
var aggExprNestedRe = regexp.MustCompile(`([a-zA-Z0-9_]+\((?:[a-zA-Z0-9_]+\([^()]*\)|[^()])*\))(?:\s*as\s+('[^']*'|"[^"]*"|[a-zA-Z0-9_-]+))?`)
// parseAggregations pulls every func(args) (with inline or passed-through alias,
// quotes stripped) out of a v1 expression. Mirrors the frontend's
// parseAggregations; empty result if none.
func parseAggregations(expression, availableAlias string) []any {
matches := aggExprRe.FindAllStringSubmatch(expression, -1)
if hasUnbalancedParens(matches) {
matches = aggExprNestedRe.FindAllStringSubmatch(expression, -1)
}
out := make([]any, 0, len(matches))
for _, m := range matches {
alias := m[2]
if alias == "" {
alias = availableAlias
}
agg := map[string]any{"expression": m[1]}
if alias != "" {
agg["alias"] = strings.Trim(alias, `'"`)
}
out = append(out, agg)
}
return out
}
// hasUnbalancedParens reports whether any matched expression has mismatched
// parens — the signature of aggExprRe truncating a nested expr ("rate(count()").
func hasUnbalancedParens(matches [][]string) bool {
for _, m := range matches {
if strings.Count(m[1], "(") != strings.Count(m[1], ")") {
return true
}
}
return false
}
// normalizePreV5SelectColumns / normalizePreV5GroupBy let WrapInV5Envelope (which
// reads the old {key,dataType,type}) handle selectColumns/groupBy stored the v5 way
// ({name,…}) — see backfillPreV5FieldKeys. Inverse of normalizePreV5FieldKeys (the
// two consumers want opposite shapes).
func normalizePreV5SelectColumns(query map[string]any) {
if cols, ok := query["selectColumns"].([]any); ok {
query["selectColumns"] = backfillPreV5FieldKeys(cols)
}
}
func normalizePreV5GroupBy(query map[string]any) {
if gb, ok := query["groupBy"].([]any); ok {
query["groupBy"] = backfillPreV5FieldKeys(gb)
}
}
// backfillPreV5FieldKeys copies v5 field names (name/fieldDataType/fieldContext)
// down to their v4 equivalents (key/dataType/type) so WrapInV5Envelope, which reads
// the v4 names, sees a field stored the v5 way. Fields with no resolvable key are
// dropped.
func backfillPreV5FieldKeys(fields []any) []any {
out := make([]any, 0, len(fields))
for _, f := range fields {
field, ok := f.(map[string]any)
if !ok {
continue
}
if _, ok := field["key"]; !ok {
if name, ok := field["name"]; ok {
field["key"] = name
}
}
if _, ok := field["dataType"]; !ok {
if fdt, ok := field["fieldDataType"]; ok {
field["dataType"] = fdt
}
}
if _, ok := field["type"]; !ok {
if fc, ok := field["fieldContext"]; ok {
field["type"] = fc
}
}
if key, _ := field["key"].(string); key == "" {
continue
}
out = append(out, field)
}
return out
}
// normalizePreV5FieldKeys renames list-panel field keys {key,dataType,type} →
// {name,fieldDataType,fieldContext} in place (as WrapInV5Envelope does for
// groupBy/orderBy). Entries already carrying "name" are left as-is.
func normalizePreV5FieldKeys(fields []any) {
for _, f := range fields {
field, ok := f.(map[string]any)
if !ok {
continue
}
if _, hasName := field["name"]; hasName {
continue
}
if key, ok := field["key"]; ok {
field["name"] = key
}
if dataType, ok := field["dataType"]; ok {
field["fieldDataType"] = dataType
}
if typ, ok := field["type"]; ok {
field["fieldContext"] = typ
}
}
}
// normalizePreV5PageSize backfills limit from the legacy pageSize (frontend's
// `limit || pageSize`), for row-limited panels (list/table) only. Leaves a query
// that already has limit, or a non-row-limited panel, untouched.
func normalizePreV5PageSize(query map[string]any, rowLimitPanel bool) {
if !rowLimitPanel {
return
}
if limit, ok := query["limit"]; ok && limit != nil {
return
}
if ps, ok := query["pageSize"]; ok {
query["limit"] = ps
}
}
// normalizeQueryLimit drops a limit above the v5 maximum (MaxQueryLimit); v1 allowed
// larger/unbounded limits, and an over-max value fails validation. Removing it leaves
// the query unlimited (the field is optional).
func normalizeQueryLimit(query map[string]any) {
limit, ok := coerceFloat(query["limit"])
if !ok {
return
}
if limit > qb.MaxQueryLimit {
delete(query, "limit")
}
}

View File

@@ -0,0 +1,122 @@
package dashboardtypes
import (
"fmt"
"regexp"
"strings"
"github.com/SigNoz/signoz/pkg/types/coretypes"
"github.com/SigNoz/signoz/pkg/types/tagtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// ══════════════════════════════════════════════
// Tags
// ══════════════════════════════════════════════
// v1 carries tags as a flat []string; v2 tags are (key, value) pairs. Each v1
// string is normalized into a pair (separator split, empty-side fallback,
// reserved-key prefix, `/` scrub). Tags that normalize to the same
// (lower(key), lower(value)) within a dashboard are collapsed, first occurrence
// winning the display casing.
//
// Characters still illegal after normalization (spaces, punctuation) are molded
// to fit the tag validators: disallowed runs collapse to "_" (see moldTagField).
// defaultV1TagKey is the key assigned when a v1 tag string has no usable
// separator (or one side of the split is empty).
const defaultV1TagKey = "tag"
func (d *v1Decoder) convertV1TagsForOrg(orgID valuer.UUID, raw any) []*tagtypes.Tag {
if raw == nil {
return nil
}
rawTagsList, ok := raw.([]any)
if !ok {
d.noteMalformedField("tags", raw)
return nil
}
seen := make(map[string]struct{}, len(rawTagsList))
tagsV2 := make([]*tagtypes.Tag, 0, len(rawTagsList))
for i, rawTag := range rawTagsList {
s, ok := rawTag.(string)
if !ok {
d.noteMalformedField(fmt.Sprintf("tags[%d]", i), rawTag)
continue
}
key, value, ok := normalizeV1Tag(s)
if !ok {
continue
}
dedupKey := strings.ToLower(key) + "\x00" + strings.ToLower(value)
if _, dup := seen[dedupKey]; dup {
continue
}
seen[dedupKey] = struct{}{}
tagsV2 = append(tagsV2, tagtypes.NewTag(orgID, coretypes.KindDashboard, key, value))
}
return tagsV2
}
// normalizeV1Tag derives a (key, value) pair from one v1 tag string. After
// splitting and molding both sides, a lone survivor becomes a value under the
// default key; ok is false if neither survives.
func normalizeV1Tag(s string) (string, string, bool) {
s = strings.TrimSpace(s)
if s == "" {
return "", "", false
}
var rawKey, rawValue string
switch {
case strings.Contains(s, ":"):
rawKey, rawValue, _ = strings.Cut(s, ":")
// Only the first ":" separates key from value; collapse the rest.
rawValue = strings.ReplaceAll(rawValue, ":", "_")
case strings.Contains(s, "/"):
rawKey, rawValue, _ = strings.Cut(s, "/")
default:
rawValue = s
}
rawKey = strings.TrimSpace(rawKey)
rawValue = strings.TrimSpace(rawValue)
// Reserved-key collision: prefix "_" so the list-query DSL stays unambiguous.
if _, reserved := reservedDSLKeys[DSLKey(strings.ToLower(rawKey))]; rawKey != "" && reserved {
rawKey = "_" + rawKey
}
key := moldTagField(rawKey, tagKeyDisallowed, tagKeyNotLead, tagtypes.MAX_LEN_TAG_KEY)
value := moldTagField(rawValue, tagValueDisallowed, nil, tagtypes.MAX_LEN_TAG_VALUE)
switch {
case key == "" && value == "":
return "", "", false
case key == "":
return defaultV1TagKey, value, true
case value == "":
return defaultV1TagKey, key, true
default:
return key, value, true
}
}
// Inverse of tagKeyRegex/tagValueRegex ("/" always rejected); tagKeyNotLead
// matches a bad first char for a key. TestMoldedV1TagsPassValidation guards drift.
var (
tagKeyDisallowed = regexp.MustCompile(`[^a-zA-Z0-9$_@#{}:-]+`)
tagValueDisallowed = regexp.MustCompile(`[^a-zA-Z0-9$_@#{}:.+=-]+`)
tagKeyNotLead = regexp.MustCompile(`^[^a-zA-Z$_@{#]`)
)
// moldTagField collapses disallowed runs to "_", prefixes "_" if notLead hits
// the first char, and caps at max. Keeps a leading "_", trims a trailing one.
func moldTagField(s string, disallowed, notLead *regexp.Regexp, max int) string {
s = strings.TrimRight(disallowed.ReplaceAllString(s, "_"), "_")
if s != "" && notLead != nil && notLead.MatchString(s) {
s = "_" + s
}
if len(s) > max {
s = strings.TrimRight(s[:max], "_")
}
return s
}

File diff suppressed because it is too large Load Diff

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