Compare commits

..

1 Commits

Author SHA1 Message Date
Vinícius Lourenço
336e827d37 feat(signozhq-ui): add @signozhq/ui lib 2026-03-17 11:24:10 -03:00
61 changed files with 384 additions and 1803 deletions

View File

@@ -102,3 +102,13 @@ jobs:
run: |
go run cmd/enterprise/*.go generate openapi
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in openapi spec. Run go run cmd/enterprise/*.go generate openapi locally and commit."; exit 1)
- name: node-install
uses: actions/setup-node@v5
with:
node-version: "22"
- name: install-frontend
run: cd frontend && yarn install
- name: generate-api-clients
run: |
cd frontend && yarn generate:api
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated api clients. Run yarn generate:api in frontend/ locally and commit."; exit 1)

View File

@@ -52,16 +52,16 @@ jobs:
with:
PRIMUS_REF: main
JS_SRC: frontend
languages:
md-languages:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
- name: checkout
uses: actions/checkout@v4
- name: run
- name: validate md languages
run: bash frontend/scripts/validate-md-languages.sh
authz:
if: |
@@ -70,55 +70,44 @@ jobs:
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
- name: Checkout code
uses: actions/checkout@v5
- name: node-install
- name: Set up Node.js
uses: actions/setup-node@v5
with:
node-version: "22"
- name: deps-install
- name: Install frontend dependencies
working-directory: ./frontend
run: |
yarn install
- name: uv-install
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: uv-deps
- name: Install Python dependencies
working-directory: ./tests/integration
run: |
uv sync
- name: setup-test
- name: Start test environment
run: |
make py-test-setup
- name: generate
- name: Generate permissions.type.ts
working-directory: ./frontend
run: |
yarn generate:permissions-type
- name: teardown-test
- name: Teardown test environment
if: always()
run: |
make py-test-teardown
- name: validate
- name: Check for changes
run: |
if ! git diff --exit-code frontend/src/hooks/useAuthZ/permissions.type.ts; then
echo "::error::frontend/src/hooks/useAuthZ/permissions.type.ts is out of date. Please run the generator locally and commit the changes: npm run generate:permissions-type (from the frontend directory)"
exit 1
fi
openapi:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: node-install
uses: actions/setup-node@v5
with:
node-version: "22"
- name: install-frontend
run: cd frontend && yarn install
- name: generate-api-clients
run: |
cd frontend && yarn generate:api
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in generated api clients. Run yarn generate:api in frontend/ locally and commit."; exit 1)

View File

@@ -220,13 +220,6 @@ components:
- additions
- deletions
type: object
AuthtypesPatchableRole:
properties:
description:
type: string
required:
- description
type: object
AuthtypesPostableAuthDomain:
properties:
config:
@@ -243,15 +236,6 @@ components:
password:
type: string
type: object
AuthtypesPostableRole:
properties:
description:
type: string
name:
type: string
required:
- name
type: object
AuthtypesPostableRotateToken:
properties:
refreshToken:
@@ -267,31 +251,6 @@ components:
- name
- type
type: object
AuthtypesRole:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
name:
type: string
orgId:
type: string
type:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- description
- type
- orgId
type: object
AuthtypesRoleMapping:
properties:
defaultRole:
@@ -1763,6 +1722,47 @@ components:
- status
- error
type: object
RoletypesPatchableRole:
properties:
description:
type: string
required:
- description
type: object
RoletypesPostableRole:
properties:
description:
type: string
name:
type: string
required:
- name
type: object
RoletypesRole:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
name:
type: string
orgId:
type: string
type:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- description
- type
- orgId
type: object
ServiceaccounttypesFactorAPIKey:
properties:
createdAt:
@@ -4234,7 +4234,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/AuthtypesRole'
$ref: '#/components/schemas/RoletypesRole'
type: array
status:
type: string
@@ -4277,7 +4277,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthtypesPostableRole'
$ref: '#/components/schemas/RoletypesPostableRole'
responses:
"201":
content:
@@ -4422,7 +4422,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesRole'
$ref: '#/components/schemas/RoletypesRole'
status:
type: string
required:
@@ -4470,7 +4470,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthtypesPatchableRole'
$ref: '#/components/schemas/RoletypesPatchableRole'
responses:
"204":
content:

View File

@@ -13,6 +13,7 @@ import (
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
@@ -22,7 +23,7 @@ type provider struct {
pkgAuthzService authz.AuthZ
openfgaServer *openfgaserver.Server
licensing licensing.Licensing
store authtypes.RoleStore
store roletypes.Store
registry []authz.RegisterTypeable
}
@@ -81,23 +82,23 @@ func (provider *provider) Write(ctx context.Context, additions []*openfgav1.Tupl
return provider.openfgaServer.Write(ctx, additions, deletions)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) {
return provider.pkgAuthzService.Get(ctx, orgID, id)
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.Role, error) {
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
}
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
return provider.pkgAuthzService.List(ctx, orgID)
}
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.Role, error) {
return provider.pkgAuthzService.ListByOrgIDAndNames(ctx, orgID, names)
}
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.Role, error) {
return provider.pkgAuthzService.ListByOrgIDAndIDs(ctx, orgID, ids)
}
@@ -113,7 +114,7 @@ func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names [
return provider.pkgAuthzService.Revoke(ctx, orgID, names, subject)
}
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*authtypes.Role) error {
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*roletypes.Role) error {
return provider.pkgAuthzService.CreateManagedRoles(ctx, orgID, managedRoles)
}
@@ -135,16 +136,16 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
return provider.Write(ctx, tuples, nil)
}
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
return provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
}
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, error) {
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) (*roletypes.Role, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -158,10 +159,10 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
}
if existingRole != nil {
return authtypes.NewRoleFromStorableRole(existingRole), nil
return roletypes.NewRoleFromStorableRole(existingRole), nil
}
err = provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
err = provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
if err != nil {
return nil, err
}
@@ -216,13 +217,13 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
return objects, nil
}
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return provider.store.Update(ctx, orgID, authtypes.NewStorableRoleFromRole(role))
return provider.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role))
}
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
@@ -231,12 +232,12 @@ func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, n
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
additionTuples, err := authtypes.GetAdditionTuples(name, orgID, relation, additions)
additionTuples, err := roletypes.GetAdditionTuples(name, orgID, relation, additions)
if err != nil {
return err
}
deletionTuples, err := authtypes.GetDeletionTuples(name, orgID, relation, deletions)
deletionTuples, err := roletypes.GetDeletionTuples(name, orgID, relation, deletions)
if err != nil {
return err
}
@@ -260,7 +261,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return err
}
role := authtypes.NewRoleFromStorableRole(storableRole)
role := roletypes.NewRoleFromStorableRole(storableRole)
err = role.ErrIfManaged()
if err != nil {
return err
@@ -270,7 +271,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{authtypes.TypeableRole, authtypes.TypeableResourcesRoles}
return []authtypes.Typeable{authtypes.TypeableRole, roletypes.TypeableResourcesRoles}
}
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) ([]*openfgav1.TupleKey, error) {
@@ -282,7 +283,7 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
adminSubject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
},
orgID,
)
@@ -297,7 +298,7 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
anonymousSubject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAnonymousRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAnonymousRoleName),
},
orgID,
)

View File

@@ -19,6 +19,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -223,7 +224,7 @@ func (module *module) MustGetTypeables() []authtypes.Typeable {
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
return map[string][]*authtypes.Transaction{
authtypes.SigNozAnonymousRoleName: {
roletypes.SigNozAnonymousRoleName: {
{
ID: valuer.GenerateUUID(),
Relation: authtypes.RelationRead,

View File

@@ -80,21 +80,6 @@ func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
alertDataRows := cmock.NewRows(cols, tc.Values)
mock := telemetryStore.Mock()
// Mock metadata queries for FetchTemporalityAndTypeMulti
// First query: fetchMetricsTemporalityAndType (from signoz_metrics time series table)
metadataCols := []cmock.ColumnType{
{Name: "metric_name", Type: "String"},
{Name: "temporality", Type: "String"},
{Name: "type", Type: "String"},
{Name: "is_monotonic", Type: "Bool"},
}
metadataRows := cmock.NewRows(metadataCols, [][]any{
{"probe_success", metrictypes.Unspecified, metrictypes.GaugeType, false},
})
mock.ExpectQuery("*distributed_time_series_v4*").WithArgs(nil, nil, nil).WillReturnRows(metadataRows)
// Second query: fetchMeterSourceMetricsTemporalityAndType (from signoz_meter table)
emptyMetadataRows := cmock.NewRows(metadataCols, [][]any{})
mock.ExpectQuery("*meter*").WithArgs(nil).WillReturnRows(emptyMetadataRows)
// Generate query arguments for the metric query
evalTime := time.Now().UTC()

View File

@@ -11,7 +11,6 @@
"prettify": "prettier --write .",
"fmt": "prettier --check .",
"lint": "eslint ./src",
"lint:generated": "eslint ./src/api/generated --fix",
"lint:fix": "eslint ./src --fix",
"jest": "jest",
"jest:coverage": "jest --coverage",
@@ -285,4 +284,4 @@
"tmp": "0.2.4",
"vite": "npm:rolldown-vite@7.3.1"
}
}
}

View File

@@ -25,7 +25,7 @@ echo "\n✅ Prettier formatting successful"
# Fix linting issues
echo "\n\n---\nRunning eslint...\n"
if ! yarn lint:generated; then
if ! yarn lint --fix --quiet src/api/generated; then
echo "ESLint check failed! Please fix linting errors before proceeding."
exit 1
fi

View File

@@ -21,8 +21,6 @@ import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
AuthtypesPatchableObjectsDTO,
AuthtypesPatchableRoleDTO,
AuthtypesPostableRoleDTO,
CreateRole201,
DeleteRolePathParameters,
GetObjects200,
@@ -33,6 +31,8 @@ import type {
PatchObjectsPathParameters,
PatchRolePathParameters,
RenderErrorResponseDTO,
RoletypesPatchableRoleDTO,
RoletypesPostableRoleDTO,
} from '../sigNoz.schemas';
/**
@@ -118,14 +118,14 @@ export const invalidateListRoles = async (
* @summary Create role
*/
export const createRole = (
authtypesPostableRoleDTO: BodyType<AuthtypesPostableRoleDTO>,
roletypesPostableRoleDTO: BodyType<RoletypesPostableRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateRole201>({
url: `/api/v1/roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: authtypesPostableRoleDTO,
data: roletypesPostableRoleDTO,
signal,
});
};
@@ -137,13 +137,13 @@ export const getCreateRoleMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<AuthtypesPostableRoleDTO> },
{ data: BodyType<RoletypesPostableRoleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<AuthtypesPostableRoleDTO> },
{ data: BodyType<RoletypesPostableRoleDTO> },
TContext
> => {
const mutationKey = ['createRole'];
@@ -157,7 +157,7 @@ export const getCreateRoleMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createRole>>,
{ data: BodyType<AuthtypesPostableRoleDTO> }
{ data: BodyType<RoletypesPostableRoleDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -170,7 +170,7 @@ export const getCreateRoleMutationOptions = <
export type CreateRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createRole>>
>;
export type CreateRoleMutationBody = BodyType<AuthtypesPostableRoleDTO>;
export type CreateRoleMutationBody = BodyType<RoletypesPostableRoleDTO>;
export type CreateRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -183,13 +183,13 @@ export const useCreateRole = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<AuthtypesPostableRoleDTO> },
{ data: BodyType<RoletypesPostableRoleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<AuthtypesPostableRoleDTO> },
{ data: BodyType<RoletypesPostableRoleDTO> },
TContext
> => {
const mutationOptions = getCreateRoleMutationOptions(options);
@@ -370,13 +370,13 @@ export const invalidateGetRole = async (
*/
export const patchRole = (
{ id }: PatchRolePathParameters,
authtypesPatchableRoleDTO: BodyType<AuthtypesPatchableRoleDTO>,
roletypesPatchableRoleDTO: BodyType<RoletypesPatchableRoleDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: authtypesPatchableRoleDTO,
data: roletypesPatchableRoleDTO,
});
};
@@ -389,7 +389,7 @@ export const getPatchRoleMutationOptions = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<AuthtypesPatchableRoleDTO>;
data: BodyType<RoletypesPatchableRoleDTO>;
},
TContext
>;
@@ -398,7 +398,7 @@ export const getPatchRoleMutationOptions = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<AuthtypesPatchableRoleDTO>;
data: BodyType<RoletypesPatchableRoleDTO>;
},
TContext
> => {
@@ -415,7 +415,7 @@ export const getPatchRoleMutationOptions = <
Awaited<ReturnType<typeof patchRole>>,
{
pathParams: PatchRolePathParameters;
data: BodyType<AuthtypesPatchableRoleDTO>;
data: BodyType<RoletypesPatchableRoleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -429,7 +429,7 @@ export const getPatchRoleMutationOptions = <
export type PatchRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof patchRole>>
>;
export type PatchRoleMutationBody = BodyType<AuthtypesPatchableRoleDTO>;
export type PatchRoleMutationBody = BodyType<RoletypesPatchableRoleDTO>;
export type PatchRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -444,7 +444,7 @@ export const usePatchRole = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<AuthtypesPatchableRoleDTO>;
data: BodyType<RoletypesPatchableRoleDTO>;
},
TContext
>;
@@ -453,7 +453,7 @@ export const usePatchRole = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<AuthtypesPatchableRoleDTO>;
data: BodyType<RoletypesPatchableRoleDTO>;
},
TContext
> => {

View File

@@ -278,13 +278,6 @@ export interface AuthtypesPatchableObjectsDTO {
deletions: AuthtypesGettableObjectsDTO[] | null;
}
export interface AuthtypesPatchableRoleDTO {
/**
* @type string
*/
description: string;
}
export interface AuthtypesPostableAuthDomainDTO {
config?: AuthtypesAuthDomainConfigDTO;
/**
@@ -308,17 +301,6 @@ export interface AuthtypesPostableEmailPasswordSessionDTO {
password?: string;
}
export interface AuthtypesPostableRoleDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
}
export interface AuthtypesPostableRotateTokenDTO {
/**
* @type string
@@ -337,39 +319,6 @@ export interface AuthtypesResourceDTO {
type: string;
}
export interface AuthtypesRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
type: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
/**
* @nullable
*/
@@ -2090,6 +2039,57 @@ export interface RenderErrorResponseDTO {
status: string;
}
export interface RoletypesPatchableRoleDTO {
/**
* @type string
*/
description: string;
}
export interface RoletypesPostableRoleDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
}
export interface RoletypesRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
type: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
export interface ServiceaccounttypesFactorAPIKeyDTO {
/**
* @type string
@@ -3163,7 +3163,7 @@ export type ListRoles200 = {
/**
* @type array
*/
data: AuthtypesRoleDTO[];
data: RoletypesRoleDTO[];
/**
* @type string
*/
@@ -3185,7 +3185,7 @@ export type GetRolePathParameters = {
id: string;
};
export type GetRole200 = {
data: AuthtypesRoleDTO;
data: RoletypesRoleDTO;
/**
* @type string
*/

View File

@@ -123,7 +123,7 @@ export const prepareUPlotConfig = ({
drawStyle: hasSingleValidPoint ? DrawStyle.Points : DrawStyle.Line,
label: label,
colorMapping: widget.customLegendColors ?? {},
spanGaps: widget.spanGaps ?? true,
spanGaps: true,
lineStyle: widget.lineStyle || LineStyle.Solid,
lineInterpolation: widget.lineInterpolation || LineInterpolation.Spline,
showPoints:

View File

@@ -337,31 +337,6 @@
.login-submit-btn {
width: 100%;
height: 32px;
padding: 10px 16px;
background: var(--primary);
border: none;
border-radius: 2px;
font-family: Inter, sans-serif;
font-size: 11px;
font-weight: 500;
line-height: 1;
color: var(--bg-neutral-dark-50);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
&:hover:not(:disabled) {
background: var(--primary);
opacity: 0.9;
}
&:disabled {
background: var(--primary);
opacity: 0.6;
cursor: not-allowed;
}
}
.lightMode {

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { Button } from '@signozhq/button';
import { Button } from '@signozhq/ui';
import { Form, Input, Select, Typography } from 'antd';
import getVersion from 'api/v1/version/get';
import get from 'api/v2/sessions/context/get';
@@ -392,9 +392,9 @@ function Login(): JSX.Element {
disabled={!isNextButtonEnabled}
variant="solid"
onClick={onNextHandler}
data-testid="initiate_login"
testId="initiate_login"
className="login-submit-btn"
suffixIcon={<ArrowRight size={12} />}
suffix={<ArrowRight />}
>
Next
</Button>
@@ -406,10 +406,10 @@ function Login(): JSX.Element {
variant="solid"
type="submit"
color="primary"
data-testid="callback_authn_submit"
testId="callback_authn_submit"
data-attr="signup"
className="login-submit-btn"
suffixIcon={<ArrowRight size={12} />}
suffix={<ArrowRight />}
>
Sign in with SSO
</Button>
@@ -420,11 +420,11 @@ function Login(): JSX.Element {
disabled={!isSubmitButtonEnabled}
variant="solid"
color="primary"
data-testid="password_authn_submit"
testId="password_authn_submit"
type="submit"
data-attr="signup"
className="login-submit-btn"
suffixIcon={<ArrowRight size={12} />}
suffix={<ArrowRight />}
>
Sign in with Password
</Button>

View File

@@ -30,17 +30,6 @@
width: 100%;
}
.section-heading {
font-family: 'Space Mono';
color: var(--bg-vanilla-400);
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 138.462% */
letter-spacing: 0.52px;
text-transform: uppercase;
}
.header {
display: flex;
padding: 14px 14px 14px 12px;

View File

@@ -7,10 +7,9 @@ import {
} from 'lib/uPlotV2/config/types';
import { Paintbrush } from 'lucide-react';
import DisconnectValuesSelector from '../../components/DisconnectValuesSelector/DisconnectValuesSelector';
import FillModeSelector from '../../components/FillModeSelector/FillModeSelector';
import LineInterpolationSelector from '../../components/LineInterpolationSelector/LineInterpolationSelector';
import LineStyleSelector from '../../components/LineStyleSelector/LineStyleSelector';
import { FillModeSelector } from '../../components/FillModeSelector/FillModeSelector';
import { LineInterpolationSelector } from '../../components/LineInterpolationSelector/LineInterpolationSelector';
import { LineStyleSelector } from '../../components/LineStyleSelector/LineStyleSelector';
import SettingsSection from '../../components/SettingsSection/SettingsSection';
interface ChartAppearanceSectionProps {
@@ -22,14 +21,10 @@ interface ChartAppearanceSectionProps {
setLineInterpolation: Dispatch<SetStateAction<LineInterpolation>>;
showPoints: boolean;
setShowPoints: Dispatch<SetStateAction<boolean>>;
spanGaps: boolean | number;
setSpanGaps: Dispatch<SetStateAction<boolean | number>>;
allowFillMode: boolean;
allowLineStyle: boolean;
allowLineInterpolation: boolean;
allowShowPoints: boolean;
allowSpanGaps: boolean;
stepInterval: number;
}
export default function ChartAppearanceSection({
@@ -41,14 +36,10 @@ export default function ChartAppearanceSection({
setLineInterpolation,
showPoints,
setShowPoints,
spanGaps,
setSpanGaps,
allowFillMode,
allowLineStyle,
allowLineInterpolation,
allowShowPoints,
allowSpanGaps,
stepInterval,
}: ChartAppearanceSectionProps): JSX.Element {
return (
<SettingsSection title="Chart Appearance" icon={<Paintbrush size={14} />}>
@@ -75,13 +66,6 @@ export default function ChartAppearanceSection({
<Switch size="small" checked={showPoints} onChange={setShowPoints} />
</section>
)}
{allowSpanGaps && (
<DisconnectValuesSelector
value={spanGaps}
minValue={stepInterval}
onChange={setSpanGaps}
/>
)}
</SettingsSection>
);
}

View File

@@ -178,8 +178,6 @@ describe('RightContainer - Alerts Section', () => {
setLineStyle: jest.fn(),
showPoints: false,
setShowPoints: jest.fn(),
spanGaps: false,
setSpanGaps: jest.fn(),
};
beforeEach(() => {

View File

@@ -1,44 +0,0 @@
import { ToggleGroup, ToggleGroupItem } from '@signozhq/toggle-group';
import { Typography } from 'antd';
import { DisconnectedValuesMode } from 'lib/uPlotV2/config/types';
interface DisconnectValuesModeToggleProps {
value: DisconnectedValuesMode;
onChange: (value: DisconnectedValuesMode) => void;
}
export default function DisconnectValuesModeToggle({
value,
onChange,
}: DisconnectValuesModeToggleProps): JSX.Element {
return (
<ToggleGroup
type="single"
value={value}
variant="outline"
size="lg"
onValueChange={(newValue): void => {
if (newValue) {
onChange(newValue as DisconnectedValuesMode);
}
}}
>
<ToggleGroupItem
value={DisconnectedValuesMode.Never}
aria-label="Never"
title="Never"
>
<Typography.Text className="section-heading-small">Never</Typography.Text>
</ToggleGroupItem>
<ToggleGroupItem
value={DisconnectedValuesMode.Threshold}
aria-label="Threshold"
title="Threshold"
>
<Typography.Text className="section-heading-small">
Threshold
</Typography.Text>
</ToggleGroupItem>
</ToggleGroup>
);
}

View File

@@ -1,21 +0,0 @@
.disconnect-values-selector {
.disconnect-values-input-wrapper {
display: flex;
flex-direction: column;
gap: 16px;
.disconnect-values-threshold-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
.disconnect-values-threshold-input {
max-width: 160px;
height: auto;
.disconnect-values-threshold-prefix {
padding: 0 8px;
font-size: 20px;
}
}
}
}
}

View File

@@ -1,91 +0,0 @@
import { useEffect, useState } from 'react';
import { Typography } from 'antd';
import { DisconnectedValuesMode } from 'lib/uPlotV2/config/types';
import DisconnectValuesModeToggle from './DisconnectValuesModeToggle';
import DisconnectValuesThresholdInput from './DisconnectValuesThresholdInput';
import './DisconnectValuesSelector.styles.scss';
const DEFAULT_THRESHOLD_SECONDS = 60;
interface DisconnectValuesSelectorProps {
value: boolean | number;
minValue?: number;
onChange: (value: boolean | number) => void;
}
export default function DisconnectValuesSelector({
value,
minValue,
onChange,
}: DisconnectValuesSelectorProps): JSX.Element {
const [mode, setMode] = useState<DisconnectedValuesMode>(() => {
if (typeof value === 'number') {
return DisconnectedValuesMode.Threshold;
}
return DisconnectedValuesMode.Never;
});
const [thresholdSeconds, setThresholdSeconds] = useState<number>(
typeof value === 'number' ? value : minValue ?? DEFAULT_THRESHOLD_SECONDS,
);
useEffect(() => {
if (typeof value === 'boolean') {
setMode(DisconnectedValuesMode.Never);
} else if (typeof value === 'number') {
setMode(DisconnectedValuesMode.Threshold);
setThresholdSeconds(value);
}
}, [value]);
useEffect(() => {
if (minValue !== undefined) {
setThresholdSeconds(minValue);
if (mode === DisconnectedValuesMode.Threshold) {
onChange(minValue);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minValue]);
const handleModeChange = (newMode: DisconnectedValuesMode): void => {
setMode(newMode);
switch (newMode) {
case DisconnectedValuesMode.Never:
onChange(true);
break;
case DisconnectedValuesMode.Threshold:
onChange(thresholdSeconds);
break;
}
};
const handleThresholdChange = (seconds: number): void => {
setThresholdSeconds(seconds);
onChange(seconds);
};
return (
<section className="disconnect-values-selector control-container">
<Typography.Text className="section-heading">
Disconnect values
</Typography.Text>
<div className="disconnect-values-input-wrapper">
<DisconnectValuesModeToggle value={mode} onChange={handleModeChange} />
{mode === DisconnectedValuesMode.Threshold && (
<section className="control-container">
<Typography.Text className="section-heading">
Threshold Value
</Typography.Text>
<DisconnectValuesThresholdInput
value={thresholdSeconds}
minValue={minValue}
onChange={handleThresholdChange}
/>
</section>
)}
</div>
</section>
);
}

View File

@@ -1,87 +0,0 @@
import { useEffect, useState } from 'react';
import { rangeUtil } from '@grafana/data';
import { Callout } from '@signozhq/callout';
import { Input } from '@signozhq/ui';
interface DisconnectValuesThresholdInputProps {
value: number;
onChange: (seconds: number) => void;
minValue?: number;
}
export default function DisconnectValuesThresholdInput({
value,
onChange,
minValue,
}: DisconnectValuesThresholdInputProps): JSX.Element {
const [inputValue, setInputValue] = useState<string>(
rangeUtil.secondsToHms(value),
);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setInputValue(rangeUtil.secondsToHms(value));
setError(null);
}, [value]);
const commitValue = (txt: string): void => {
if (!txt) {
return;
}
try {
let seconds: number;
if (rangeUtil.isValidTimeSpan(txt)) {
seconds = rangeUtil.intervalToSeconds(txt);
} else {
const parsed = Number(txt);
if (Number.isNaN(parsed) || parsed <= 0) {
setError('Enter a valid duration (e.g. 1h, 10m, 1d)');
return;
}
seconds = parsed;
}
if (minValue !== undefined && seconds <= minValue) {
setError(`Threshold should be > ${rangeUtil.secondsToHms(minValue)}`);
return;
}
setError(null);
setInputValue(txt);
onChange(seconds);
} catch {
setError('Invalid threshold value');
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
commitValue(e.currentTarget.value);
}
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>): void => {
commitValue(e.currentTarget.value);
};
return (
<div className="disconnect-values-threshold-wrapper">
<Input
name="disconnect-values-threshold"
type="text"
className="disconnect-values-threshold-input"
prefix={<span className="disconnect-values-threshold-prefix">&gt;</span>}
value={inputValue}
onChange={(e): void => {
setInputValue(e.currentTarget.value);
if (error) {
setError(null);
}
}}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
autoFocus={false}
aria-invalid={!!error}
aria-describedby={error ? 'threshold-error' : undefined}
/>
{error && <Callout type="error" size="small" showIcon description={error} />}
</div>
);
}

View File

@@ -9,7 +9,7 @@ interface FillModeSelectorProps {
onChange: (value: FillMode) => void;
}
export default function FillModeSelector({
export function FillModeSelector({
value,
onChange,
}: FillModeSelectorProps): JSX.Element {

View File

@@ -9,7 +9,7 @@ interface LineInterpolationSelectorProps {
onChange: (value: LineInterpolation) => void;
}
export default function LineInterpolationSelector({
export function LineInterpolationSelector({
value,
onChange,
}: LineInterpolationSelectorProps): JSX.Element {

View File

@@ -9,7 +9,7 @@ interface LineStyleSelectorProps {
onChange: (value: LineStyle) => void;
}
export default function LineStyleSelector({
export function LineStyleSelector({
value,
onChange,
}: LineStyleSelectorProps): JSX.Element {

View File

@@ -262,17 +262,3 @@ export const panelTypeVsShowPoints: {
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
export const panelTypeVsSpanGaps: {
[key in PANEL_TYPES]: boolean;
} = {
[PANEL_TYPES.TIME_SERIES]: true,
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.PIE]: false,
[PANEL_TYPES.BAR]: false,
[PANEL_TYPES.HISTOGRAM]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;

View File

@@ -1,7 +1,6 @@
import { Dispatch, SetStateAction, useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { Typography } from 'antd';
import { ExecStats } from 'api/v5/v5';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
import { PanelTypesWithData } from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
@@ -12,7 +11,6 @@ import {
LineInterpolation,
LineStyle,
} from 'lib/uPlotV2/config/types';
import get from 'lodash-es/get';
import { SuccessResponse } from 'types/api';
import {
ColumnUnit,
@@ -38,7 +36,6 @@ import {
panelTypeVsPanelTimePreferences,
panelTypeVsShowPoints,
panelTypeVsSoftMinMax,
panelTypeVsSpanGaps,
panelTypeVsStackingChartPreferences,
panelTypeVsThreshold,
panelTypeVsYAxisUnit,
@@ -71,8 +68,6 @@ function RightContainer({
setLineStyle,
showPoints,
setShowPoints,
spanGaps,
setSpanGaps,
bucketCount,
bucketWidth,
stackedBarChart,
@@ -143,7 +138,6 @@ function RightContainer({
const allowLineStyle = panelTypeVsLineStyle[selectedGraph];
const allowFillMode = panelTypeVsFillMode[selectedGraph];
const allowShowPoints = panelTypeVsShowPoints[selectedGraph];
const allowSpanGaps = panelTypeVsSpanGaps[selectedGraph];
const decimapPrecisionOptions = useMemo(
() => [
@@ -182,26 +176,10 @@ function RightContainer({
(allowFillMode ||
allowLineStyle ||
allowLineInterpolation ||
allowShowPoints ||
allowSpanGaps),
[
allowFillMode,
allowLineStyle,
allowLineInterpolation,
allowShowPoints,
allowSpanGaps,
],
allowShowPoints),
[allowFillMode, allowLineStyle, allowLineInterpolation, allowShowPoints],
);
const stepInterval = useMemo(() => {
const stepIntervals: ExecStats['stepIntervals'] = get(
queryResponse,
'data.payload.data.newResult.meta.stepIntervals',
{},
);
return Math.min(...Object.values(stepIntervals));
}, [queryResponse]);
return (
<div className="right-container">
<section className="header">
@@ -259,14 +237,10 @@ function RightContainer({
setLineInterpolation={setLineInterpolation}
showPoints={showPoints}
setShowPoints={setShowPoints}
spanGaps={spanGaps}
setSpanGaps={setSpanGaps}
allowFillMode={allowFillMode}
allowLineStyle={allowLineStyle}
allowLineInterpolation={allowLineInterpolation}
allowShowPoints={allowShowPoints}
allowSpanGaps={allowSpanGaps}
stepInterval={stepInterval}
/>
)}
@@ -390,8 +364,6 @@ export interface RightContainerProps {
setLineStyle: Dispatch<SetStateAction<LineStyle>>;
showPoints: boolean;
setShowPoints: Dispatch<SetStateAction<boolean>>;
spanGaps: boolean | number;
setSpanGaps: Dispatch<SetStateAction<boolean | number>>;
}
RightContainer.defaultProps = {

View File

@@ -220,9 +220,6 @@ function NewWidget({
const [showPoints, setShowPoints] = useState<boolean>(
selectedWidget?.showPoints ?? false,
);
const [spanGaps, setSpanGaps] = useState<boolean | number>(
selectedWidget?.spanGaps ?? false,
);
const [customLegendColors, setCustomLegendColors] = useState<
Record<string, string>
>(selectedWidget?.customLegendColors || {});
@@ -292,7 +289,6 @@ function NewWidget({
fillMode,
lineStyle,
showPoints,
spanGaps,
columnUnits,
bucketCount,
stackedBarChart,
@@ -332,7 +328,6 @@ function NewWidget({
fillMode,
lineStyle,
showPoints,
spanGaps,
customLegendColors,
contextLinks,
selectedWidget.columnWidths,
@@ -546,7 +541,6 @@ function NewWidget({
softMin: selectedWidget?.softMin || 0,
softMax: selectedWidget?.softMax || 0,
fillSpans: selectedWidget?.fillSpans,
spanGaps: selectedWidget?.spanGaps ?? true,
isLogScale: selectedWidget?.isLogScale || false,
bucketWidth: selectedWidget?.bucketWidth || 0,
bucketCount: selectedWidget?.bucketCount || 0,
@@ -578,7 +572,6 @@ function NewWidget({
softMin: selectedWidget?.softMin || 0,
softMax: selectedWidget?.softMax || 0,
fillSpans: selectedWidget?.fillSpans,
spanGaps: selectedWidget?.spanGaps ?? true,
isLogScale: selectedWidget?.isLogScale || false,
bucketWidth: selectedWidget?.bucketWidth || 0,
bucketCount: selectedWidget?.bucketCount || 0,
@@ -896,8 +889,6 @@ function NewWidget({
setLineStyle={setLineStyle}
showPoints={showPoints}
setShowPoints={setShowPoints}
spanGaps={spanGaps}
setSpanGaps={setSpanGaps}
opacity={opacity}
yAxisUnit={yAxisUnit}
columnUnits={columnUnits}

View File

@@ -13,8 +13,8 @@ import {
usePatchRole,
} from 'api/generated/services/role';
import {
AuthtypesPostableRoleDTO,
RenderErrorResponseDTO,
RoletypesPostableRoleDTO,
} from 'api/generated/services/sigNoz.schemas';
import { ErrorType } from 'api/generatedAPIInstance';
import ROUTES from 'constants/routes';
@@ -114,7 +114,7 @@ function CreateRoleModal({
data: { description: values.description || '' },
});
} else {
const data: AuthtypesPostableRoleDTO = {
const data: RoletypesPostableRoleDTO = {
name: values.name,
...(values.description ? { description: values.description } : {}),
};

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import { Pagination, Skeleton } from 'antd';
import { useListRoles } from 'api/generated/services/role';
import { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import { RoletypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
@@ -20,7 +20,7 @@ const PAGE_SIZE = 20;
type DisplayItem =
| { type: 'section'; label: string; count?: number }
| { type: 'role'; role: AuthtypesRoleDTO };
| { type: 'role'; role: RoletypesRoleDTO };
interface RolesListingTableProps {
searchQuery: string;
@@ -187,7 +187,7 @@ function RolesListingTable({
};
// todo: use table from periscope when its available for consumption
const renderRow = (role: AuthtypesRoleDTO): JSX.Element => (
const renderRow = (role: RoletypesRoleDTO): JSX.Element => (
<div
key={role.id}
className={`roles-table-row ${

View File

@@ -7,7 +7,6 @@ import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFall
import uPlot, { AlignedData, Options } from 'uplot';
import { usePlotContext } from '../context/PlotContext';
import { applySpanGapsToAlignedData } from '../utils/dataUtils';
import { UPlotChartProps } from './types';
/**
@@ -85,13 +84,7 @@ export default function UPlotChart({
} as Options;
// Create new plot instance
const seriesSpanGaps = config.getSeriesSpanGapsOptions();
const preparedData =
seriesSpanGaps.length > 0
? applySpanGapsToAlignedData(data, seriesSpanGaps)
: (data as AlignedData);
const plot = new uPlot(plotConfig, preparedData, containerRef.current);
const plot = new uPlot(plotConfig, data as AlignedData, containerRef.current);
if (plotRef) {
plotRef(plot);
@@ -169,13 +162,7 @@ export default function UPlotChart({
}
// Update data if only data changed
else if (!sameData(prevProps, currentProps) && plotInstanceRef.current) {
const seriesSpanGaps = config.getSeriesSpanGapsOptions?.() ?? [];
const preparedData =
seriesSpanGaps.length > 0
? applySpanGapsToAlignedData(data as AlignedData, seriesSpanGaps)
: (data as AlignedData);
plotInstanceRef.current.setData(preparedData as AlignedData);
plotInstanceRef.current.setData(data as AlignedData);
}
prevPropsRef.current = currentProps;

View File

@@ -86,7 +86,6 @@ const createMockConfig = (): UPlotConfigBuilder => {
}),
getId: jest.fn().mockReturnValue(undefined),
getShouldSaveSelectionPreference: jest.fn().mockReturnValue(false),
getSeriesSpanGapsOptions: jest.fn().mockReturnValue([]),
} as unknown) as UPlotConfigBuilder;
};
@@ -329,78 +328,6 @@ describe('UPlotChart', () => {
});
});
describe('spanGaps data transformation', () => {
it('inserts null break points before passing data to uPlot when a gap exceeds the numeric threshold', () => {
const config = createMockConfig();
// gap 0→100 = 100 > threshold 50 → null inserted at midpoint x=50
(config.getSeriesSpanGapsOptions as jest.Mock).mockReturnValue([
{ spanGaps: 50 },
]);
const data: AlignedData = [
[0, 100],
[1, 2],
];
render(<UPlotChart config={config} data={data} width={600} height={400} />, {
wrapper: Wrapper,
});
const [, receivedData] = mockUPlotConstructor.mock.calls[0];
expect(receivedData[0]).toEqual([0, 50, 100]);
expect(receivedData[1]).toEqual([1, null, 2]);
});
it('passes data through unchanged when no gap exceeds the numeric threshold', () => {
const config = createMockConfig();
// all gaps = 10, threshold = 50 → no insertions, same reference returned
(config.getSeriesSpanGapsOptions as jest.Mock).mockReturnValue([
{ spanGaps: 50 },
]);
const data: AlignedData = [
[0, 10, 20],
[1, 2, 3],
];
render(<UPlotChart config={config} data={data} width={600} height={400} />, {
wrapper: Wrapper,
});
const [, receivedData] = mockUPlotConstructor.mock.calls[0];
expect(receivedData).toBe(data);
});
it('transforms data passed to setData when data updates and a new gap exceeds the threshold', () => {
const config = createMockConfig();
(config.getSeriesSpanGapsOptions as jest.Mock).mockReturnValue([
{ spanGaps: 50 },
]);
// initial render: gap 10 < 50, no transformation
const initialData: AlignedData = [
[0, 10],
[1, 2],
];
// updated data: gap 100 > 50 → null inserted at midpoint x=50
const newData: AlignedData = [
[0, 100],
[3, 4],
];
const { rerender } = render(
<UPlotChart config={config} data={initialData} width={600} height={400} />,
{ wrapper: Wrapper },
);
rerender(
<UPlotChart config={config} data={newData} width={600} height={400} />,
);
const receivedData = instances[0].setData.mock.calls[0][0];
expect(receivedData[0]).toEqual([0, 50, 100]);
expect(receivedData[1]).toEqual([3, null, 4]);
});
});
describe('prop updates', () => {
it('calls setData without recreating the plot when only data changes', () => {
const config = createMockConfig();

View File

@@ -14,7 +14,6 @@ import {
STEP_INTERVAL_MULTIPLIER,
} from '../constants';
import { calculateWidthBasedOnStepInterval } from '../utils';
import { SeriesSpanGapsOption } from '../utils/dataUtils';
import {
ConfigBuilder,
ConfigBuilderProps,
@@ -162,13 +161,6 @@ export class UPlotConfigBuilder extends ConfigBuilder<
this.series.push(new UPlotSeriesBuilder(props));
}
getSeriesSpanGapsOptions(): SeriesSpanGapsOption[] {
return this.series.map((s) => {
const { spanGaps } = s.props;
return { spanGaps };
});
}
/**
* Add a hook for extensibility
*/

View File

@@ -4,7 +4,6 @@ import { calculateWidthBasedOnStepInterval } from 'lib/uPlotV2/utils';
import uPlot, { Series } from 'uplot';
import { generateGradientFill } from '../utils/generateGradientFill';
import { isolatedPointFilter } from '../utils/seriesPointsFilter';
import {
BarAlignment,
ConfigBuilder,
@@ -147,8 +146,20 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
}: {
resolvedLineColor: string;
}): Partial<Series.Points> {
const { lineWidth, pointSize, pointsFilter } = this.props;
const {
lineWidth,
pointSize,
pointsBuilder,
pointsFilter,
drawStyle,
showPoints,
} = this.props;
/**
* If pointSize is not provided, use the lineWidth * POINT_SIZE_FACTOR
* to determine the point size.
* POINT_SIZE_FACTOR is 2, so the point size will be 2x the line width.
*/
const resolvedPointSize =
pointSize ?? (lineWidth ?? DEFAULT_LINE_WIDTH) * POINT_SIZE_FACTOR;
@@ -157,44 +168,19 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
fill: resolvedLineColor,
size: resolvedPointSize,
filter: pointsFilter || undefined,
show: this.resolvePointsShow(),
};
// When spanGaps is in threshold (numeric) mode, points hidden by default
// become invisible when isolated by injected gap-nulls (no line connects
// to them). Use a gap-based filter to show only those isolated points as
// dots. Do NOT set show=true here — the filter is called with show=false
// and returns specific indices to render; setting show=true would cause
// uPlot to call filter with show=true which short-circuits the logic and
// renders all points.
if (this.shouldApplyIsolatedPointFilter(pointsConfig.show)) {
pointsConfig.filter = isolatedPointFilter;
if (pointsBuilder) {
pointsConfig.show = pointsBuilder;
} else if (drawStyle === DrawStyle.Points) {
pointsConfig.show = true;
} else {
pointsConfig.show = !!showPoints;
}
return pointsConfig;
}
private resolvePointsShow(): Series.Points['show'] {
const { pointsBuilder, drawStyle, showPoints } = this.props;
if (pointsBuilder) {
return pointsBuilder;
}
if (drawStyle === DrawStyle.Points) {
return true;
}
return !!showPoints;
}
private shouldApplyIsolatedPointFilter(show: Series.Points['show']): boolean {
const { drawStyle, spanGaps, pointsFilter } = this.props;
return (
drawStyle === DrawStyle.Line &&
typeof spanGaps === 'number' &&
!pointsFilter &&
!show
);
}
private getLineColor(): string {
const { colorMapping, label, lineColor, isDarkMode } = this.props;
if (!label) {
@@ -226,12 +212,7 @@ export class UPlotSeriesBuilder extends ConfigBuilder<SeriesProps, Series> {
return {
scale: scaleKey,
label,
// When spanGaps is numeric, we always disable uPlot's internal
// spanGaps behavior and rely on data-prep to implement the
// threshold-based null handling. When spanGaps is boolean we
// map it directly. When spanGaps is undefined we fall back to
// the default of false.
spanGaps: typeof spanGaps === 'number' ? false : !!spanGaps,
spanGaps: typeof spanGaps === 'boolean' ? spanGaps : false,
value: (): string => '',
pxAlign: true,
show,

View File

@@ -1,7 +1,6 @@
import { themeColors } from 'constants/theme';
import uPlot from 'uplot';
import { isolatedPointFilter } from '../../utils/seriesPointsFilter';
import type { SeriesProps } from '../types';
import { DrawStyle, LineInterpolation, LineStyle } from '../types';
import { POINT_SIZE_FACTOR, UPlotSeriesBuilder } from '../UPlotSeriesBuilder';
@@ -41,37 +40,6 @@ describe('UPlotSeriesBuilder', () => {
expect(typeof config.value).toBe('function');
});
it('maps boolean spanGaps directly to uPlot spanGaps', () => {
const trueBuilder = new UPlotSeriesBuilder(
createBaseProps({
spanGaps: true,
}),
);
const falseBuilder = new UPlotSeriesBuilder(
createBaseProps({
spanGaps: false,
}),
);
const trueConfig = trueBuilder.getConfig();
const falseConfig = falseBuilder.getConfig();
expect(trueConfig.spanGaps).toBe(true);
expect(falseConfig.spanGaps).toBe(false);
});
it('disables uPlot spanGaps when spanGaps is a number', () => {
const builder = new UPlotSeriesBuilder(
createBaseProps({
spanGaps: 10000,
}),
);
const config = builder.getConfig();
expect(config.spanGaps).toBe(false);
});
it('uses explicit lineColor when provided, regardless of mapping', () => {
const builder = new UPlotSeriesBuilder(
createBaseProps({
@@ -316,50 +284,4 @@ describe('UPlotSeriesBuilder', () => {
expect(config.points?.filter).toBe(pointsFilter);
});
it('assigns isolatedPointFilter and does not force show=true when spanGaps is numeric and no custom filter', () => {
const builder = new UPlotSeriesBuilder(
createBaseProps({
drawStyle: DrawStyle.Line,
spanGaps: 10_000,
showPoints: false,
}),
);
const config = builder.getConfig();
expect(config.points?.filter).toBe(isolatedPointFilter);
expect(config.points?.show).toBe(false);
});
it('does not assign isolatedPointFilter when a custom pointsFilter is provided alongside numeric spanGaps', () => {
const customFilter: uPlot.Series.Points.Filter = jest.fn(() => null);
const builder = new UPlotSeriesBuilder(
createBaseProps({
drawStyle: DrawStyle.Line,
spanGaps: 10_000,
pointsFilter: customFilter,
}),
);
const config = builder.getConfig();
expect(config.points?.filter).toBe(customFilter);
});
it('does not assign isolatedPointFilter when showPoints is true even with numeric spanGaps', () => {
const builder = new UPlotSeriesBuilder(
createBaseProps({
drawStyle: DrawStyle.Line,
spanGaps: 10_000,
showPoints: true,
}),
);
const config = builder.getConfig();
expect(config.points?.filter).toBeUndefined();
expect(config.points?.show).toBe(true);
});
});

View File

@@ -99,11 +99,6 @@ export interface ScaleProps {
distribution?: DistributionType;
}
export enum DisconnectedValuesMode {
Never = 'never',
Threshold = 'threshold',
}
/**
* Props for configuring a series
*/
@@ -180,16 +175,7 @@ export interface SeriesProps extends LineConfig, PointsConfig, BarConfig {
pointsFilter?: Series.Points.Filter;
pointsBuilder?: Series.Points.Show;
show?: boolean;
/**
* Controls how nulls are treated for this series.
*
* - boolean: mapped directly to uPlot's spanGaps behavior
* - number: interpreted as an X-axis threshold (same unit as ref values),
* where gaps smaller than this threshold are spanned by
* converting short null runs to undefined during data prep
* while uPlot's internal spanGaps is kept disabled.
*/
spanGaps?: boolean | number;
spanGaps?: boolean;
fillColor?: string;
fillMode?: FillMode;
isDarkMode?: boolean;

View File

@@ -1,12 +1,4 @@
import uPlot from 'uplot';
import {
applySpanGapsToAlignedData,
insertLargeGapNullsIntoAlignedData,
isInvalidPlotValue,
normalizePlotValue,
SeriesSpanGapsOption,
} from '../dataUtils';
import { isInvalidPlotValue, normalizePlotValue } from '../dataUtils';
describe('dataUtils', () => {
describe('isInvalidPlotValue', () => {
@@ -67,217 +59,4 @@ describe('dataUtils', () => {
expect(normalizePlotValue(42.5)).toBe(42.5);
});
});
describe('insertLargeGapNullsIntoAlignedData', () => {
it('returns original data unchanged when no gap exceeds the threshold', () => {
// all gaps = 10, threshold = 25 → no insertions
const data: uPlot.AlignedData = [
[0, 10, 20, 30],
[1, 2, 3, 4],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 25 }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result).toBe(data);
});
it('does not insert when the gap equals the threshold exactly', () => {
// gap = 50, threshold = 50 → condition is gap > threshold, not >=
const data: uPlot.AlignedData = [
[0, 50],
[1, 2],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result).toBe(data);
});
it('inserts a null at the midpoint when a single gap exceeds the threshold', () => {
// gap 0→100 = 100 > 50 → insert null at x=50
const data: uPlot.AlignedData = [
[0, 100],
[1, 2],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result[0]).toEqual([0, 50, 100]);
expect(result[1]).toEqual([1, null, 2]);
});
it('inserts nulls at every gap that exceeds the threshold', () => {
// gaps: 0→100=100, 100→110=10, 110→210=100; threshold=50
// → insert at 0→100 and 110→210
const data: uPlot.AlignedData = [
[0, 100, 110, 210],
[1, 2, 3, 4],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result[0]).toEqual([0, 50, 100, 110, 160, 210]);
expect(result[1]).toEqual([1, null, 2, 3, null, 4]);
});
it('inserts null for all series at a gap triggered by any one series', () => {
// series 0: threshold=50, gap=100 → triggers insertion
// series 1: threshold=200, gap=100 → would not trigger alone
// result: both series get null at the inserted x because the x-axis is shared
const data: uPlot.AlignedData = [
[0, 100],
[1, 2],
[3, 4],
];
const options: SeriesSpanGapsOption[] = [
{ spanGaps: 50 },
{ spanGaps: 200 },
];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result[0]).toEqual([0, 50, 100]);
expect(result[1]).toEqual([1, null, 2]);
expect(result[2]).toEqual([3, null, 4]);
});
it('ignores boolean spanGaps options (only numeric values trigger insertion)', () => {
const data: uPlot.AlignedData = [
[0, 100],
[1, 2],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: true }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result).toBe(data);
});
it('returns original data when series options array is empty', () => {
const data: uPlot.AlignedData = [
[0, 100],
[1, 2],
];
const result = insertLargeGapNullsIntoAlignedData(data, []);
expect(result).toBe(data);
});
it('returns original data when there is only one x point', () => {
const data: uPlot.AlignedData = [[0], [1]];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 10 }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result).toBe(data);
});
it('preserves existing null values in the series alongside inserted ones', () => {
// original series already has a null; gap 0→100 also triggers insertion
const data: uPlot.AlignedData = [
[0, 100, 110],
[1, null, 2],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
const result = insertLargeGapNullsIntoAlignedData(data, options);
expect(result[0]).toEqual([0, 50, 100, 110]);
expect(result[1]).toEqual([1, null, null, 2]);
});
});
describe('applySpanGapsToAlignedData', () => {
const xs: uPlot.AlignedData[0] = [0, 10, 20, 30];
it('returns original data when there are no series', () => {
const data: uPlot.AlignedData = [xs];
const result = applySpanGapsToAlignedData(data, []);
expect(result).toBe(data);
});
it('leaves data unchanged when spanGaps is undefined', () => {
const ys = [1, null, 2, null];
const data: uPlot.AlignedData = [xs, ys];
const options: SeriesSpanGapsOption[] = [{}];
const result = applySpanGapsToAlignedData(data, options);
expect(result[1]).toEqual(ys);
});
it('converts nulls to undefined when spanGaps is true', () => {
const ys = [1, null, 2, null];
const data: uPlot.AlignedData = [xs, ys];
const options: SeriesSpanGapsOption[] = [{ spanGaps: true }];
const result = applySpanGapsToAlignedData(data, options);
expect(result[1]).toEqual([1, undefined, 2, undefined]);
});
it('leaves data unchanged when spanGaps is false', () => {
const ys = [1, null, 2, null];
const data: uPlot.AlignedData = [xs, ys];
const options: SeriesSpanGapsOption[] = [{ spanGaps: false }];
const result = applySpanGapsToAlignedData(data, options);
expect(result[1]).toEqual(ys);
});
it('inserts a null break point when a gap exceeds the numeric threshold', () => {
// gap 0→100 = 100 > 50 → null inserted at midpoint x=50
const data: uPlot.AlignedData = [
[0, 100, 110],
[1, 2, 3],
];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 50 }];
const result = applySpanGapsToAlignedData(data, options);
expect(result[0]).toEqual([0, 50, 100, 110]);
expect(result[1]).toEqual([1, null, 2, 3]);
});
it('returns original data when no gap exceeds the numeric threshold', () => {
// all gaps = 10, threshold = 25 → no insertions
const data: uPlot.AlignedData = [xs, [1, 2, 3, 4]];
const options: SeriesSpanGapsOption[] = [{ spanGaps: 25 }];
const result = applySpanGapsToAlignedData(data, options);
expect(result).toBe(data);
});
it('applies both numeric gap insertion and boolean null-to-undefined in one pass', () => {
// series 0: spanGaps: 50 → gap 0→100 triggers a null break at midpoint x=50
// series 1: spanGaps: true → the inserted null at x=50 becomes undefined,
// so the line spans over it rather than breaking
const data: uPlot.AlignedData = [
[0, 100],
[1, 2],
[3, 4],
];
const options: SeriesSpanGapsOption[] = [
{ spanGaps: 50 },
{ spanGaps: true },
];
const result = applySpanGapsToAlignedData(data, options);
// x-axis extended with the inserted midpoint
expect(result[0]).toEqual([0, 50, 100]);
// series 0: null at midpoint breaks the line
expect(result[1]).toEqual([1, null, 2]);
// series 1: null at midpoint converted to undefined → line spans over it
expect(result[2]).toEqual([3, undefined, 4]);
});
});
});

View File

@@ -1,251 +0,0 @@
import type uPlot from 'uplot';
import {
findNearestNonNull,
findSandwichedIndices,
isolatedPointFilter,
} from '../seriesPointsFilter';
// ---------------------------------------------------------------------------
// Minimal uPlot stub — only the surface used by seriesPointsFilter
// ---------------------------------------------------------------------------
function makeUPlot({
xData,
yData,
idxs,
valToPosFn,
posToIdxFn,
}: {
xData: number[];
yData: (number | null | undefined)[];
idxs?: [number, number];
valToPosFn?: (val: number) => number;
posToIdxFn?: (pos: number) => number;
}): uPlot {
return ({
data: [xData, yData],
series: [{}, { idxs: idxs ?? [0, yData.length - 1] }],
valToPos: jest.fn((val: number) => (valToPosFn ? valToPosFn(val) : val)),
posToIdx: jest.fn((pos: number) =>
posToIdxFn ? posToIdxFn(pos) : Math.round(pos),
),
} as unknown) as uPlot;
}
// ---------------------------------------------------------------------------
// findNearestNonNull
// ---------------------------------------------------------------------------
describe('findNearestNonNull', () => {
it('returns the right neighbor when left side is null', () => {
const yData = [null, null, 42, null];
expect(findNearestNonNull(yData, 1)).toBe(2);
});
it('returns the left neighbor when right side is null', () => {
const yData = [null, 42, null, null];
expect(findNearestNonNull(yData, 2)).toBe(1);
});
it('prefers the right neighbor over the left when both exist at the same distance', () => {
const yData = [10, null, 20];
// j=1: right (idx 3) is out of bounds (undefined == null), left (idx 1) is null
// Actually right (idx 2) exists at j=1
expect(findNearestNonNull(yData, 1)).toBe(2);
});
it('returns approxIdx unchanged when no non-null value is found within 100 steps', () => {
const yData: (number | null)[] = Array(5).fill(null);
expect(findNearestNonNull(yData, 2)).toBe(2);
});
it('handles undefined values the same as null', () => {
const yData: (number | null | undefined)[] = [undefined, undefined, 99];
expect(findNearestNonNull(yData, 0)).toBe(2);
});
});
// ---------------------------------------------------------------------------
// findSandwichedIndices
// ---------------------------------------------------------------------------
describe('findSandwichedIndices', () => {
it('returns empty array when no consecutive gaps share a pixel boundary', () => {
const gaps = [
[0, 10],
[20, 30],
];
const yData = [1, null, null, 2];
const u = makeUPlot({ xData: [0, 1, 2, 3], yData });
expect(findSandwichedIndices(gaps, yData, u)).toEqual([]);
});
it('returns the index between two gaps that share a pixel boundary', () => {
// gaps[0] ends at 10, gaps[1] starts at 10 → sandwiched point at pixel 10
const gaps = [
[0, 10],
[10, 20],
];
// posToIdx(10) → 2
const yData = [null, null, 5, null, null];
const u = makeUPlot({ xData: [0, 1, 2, 3, 4], yData, posToIdxFn: () => 2 });
expect(findSandwichedIndices(gaps, yData, u)).toEqual([2]);
});
it('scans to nearest non-null when posToIdx lands on a null', () => {
// posToIdx returns 2 which is null; nearest non-null is index 3
const gaps = [
[0, 10],
[10, 20],
];
const yData = [null, null, null, 7, null];
const u = makeUPlot({ xData: [0, 1, 2, 3, 4], yData, posToIdxFn: () => 2 });
expect(findSandwichedIndices(gaps, yData, u)).toEqual([3]);
});
it('returns multiple indices when several gap pairs share boundaries', () => {
// Three consecutive gaps: [0,10], [10,20], [20,30]
// → two sandwiched points: between gaps 0-1 at px 10, between gaps 1-2 at px 20
const gaps = [
[0, 10],
[10, 20],
[20, 30],
];
const yData = [null, 1, null, 2, null];
const u = makeUPlot({
xData: [0, 1, 2, 3, 4],
yData,
posToIdxFn: (pos) => (pos === 10 ? 1 : 3),
});
expect(findSandwichedIndices(gaps, yData, u)).toEqual([1, 3]);
});
});
// ---------------------------------------------------------------------------
// isolatedPointFilter
// ---------------------------------------------------------------------------
describe('isolatedPointFilter', () => {
it('returns null when show is true (normal point rendering active)', () => {
const u = makeUPlot({ xData: [0, 1], yData: [1, null] });
expect(isolatedPointFilter(u, 1, true, [[0, 10]])).toBeNull();
});
it('returns null when gaps is null', () => {
const u = makeUPlot({ xData: [0, 1], yData: [1, null] });
expect(isolatedPointFilter(u, 1, false, null)).toBeNull();
});
it('returns null when gaps is empty', () => {
const u = makeUPlot({ xData: [0, 1], yData: [1, null] });
expect(isolatedPointFilter(u, 1, false, [])).toBeNull();
});
it('returns null when series idxs is undefined', () => {
const u = ({
data: [
[0, 1],
[1, null],
],
series: [{}, { idxs: undefined }],
valToPos: jest.fn(() => 0),
posToIdx: jest.fn(() => 0),
} as unknown) as uPlot;
expect(isolatedPointFilter(u, 1, false, [[0, 10]])).toBeNull();
});
it('includes firstIdx when the first gap starts at the first data point pixel', () => {
// xData[firstIdx=0] → valToPos → 5; gaps[0][0] === 5 → isolated leading point
const xData = [0, 1, 2, 3, 4];
const yData = [10, null, null, null, 20];
const u = makeUPlot({
xData,
yData,
idxs: [0, 4],
valToPosFn: (val) => (val === 0 ? 5 : 40), // firstPos=5, lastPos=40
});
// gaps[0][0] === 5 (firstPos), gaps last end !== 40
const result = isolatedPointFilter(u, 1, false, [
[5, 15],
[20, 30],
]);
expect(result).toContain(0); // firstIdx
});
it('includes lastIdx when the last gap ends at the last data point pixel', () => {
const xData = [0, 1, 2, 3, 4];
const yData = [10, null, null, null, 20];
const u = makeUPlot({
xData,
yData,
idxs: [0, 4],
valToPosFn: (val) => (val === 0 ? 5 : 40), // firstPos=5, lastPos=40
});
// gaps last end === 40 (lastPos), gaps[0][0] !== 5
const result = isolatedPointFilter(u, 1, false, [
[10, 20],
[30, 40],
]);
expect(result).toContain(4); // lastIdx
});
it('includes sandwiched index between two gaps sharing a pixel boundary', () => {
const xData = [0, 1, 2, 3, 4];
const yData = [null, null, 5, null, null];
const u = makeUPlot({
xData,
yData,
idxs: [0, 4],
valToPosFn: () => 99, // firstPos/lastPos won't match gap boundaries
posToIdxFn: () => 2,
});
const result = isolatedPointFilter(u, 1, false, [
[0, 50],
[50, 100],
]);
expect(result).toContain(2);
});
it('returns null when no isolated points are found', () => {
const xData = [0, 1, 2];
const yData = [1, 2, 3];
const u = makeUPlot({
xData,
yData,
idxs: [0, 2],
// firstPos = 10, lastPos = 30 — neither matches any gap boundary
valToPosFn: (val) => (val === 0 ? 10 : 30),
});
// gaps don't share boundaries and don't touch firstPos/lastPos
const result = isolatedPointFilter(u, 1, false, [
[0, 5],
[15, 20],
]);
expect(result).toBeNull();
});
it('returns all three kinds of isolated points in one pass', () => {
// Leading (firstPos=0 === gaps[0][0]), sandwiched (gaps[1] and gaps[2] share 50),
// trailing (lastPos=100 === gaps last end)
const xData = [0, 1, 2, 3, 4];
const yData = [1, null, 2, null, 3];
const u = makeUPlot({
xData,
yData,
idxs: [0, 4],
valToPosFn: (val) => (val === 0 ? 0 : 100),
posToIdxFn: () => 2, // sandwiched point at idx 2
});
const gaps = [
[0, 20],
[40, 50],
[50, 80],
[90, 100],
];
const result = isolatedPointFilter(u, 1, false, gaps);
expect(result).toContain(0); // leading
expect(result).toContain(2); // sandwiched
expect(result).toContain(4); // trailing
});
});

View File

@@ -51,163 +51,3 @@ export function normalizePlotValue(
// Already a valid number
return value as number;
}
export interface SeriesSpanGapsOption {
spanGaps?: boolean | number;
}
// Internal type alias: a series value array that may contain nulls/undefineds.
// uPlot uses null to draw a visible gap and undefined to represent "no sample"
// (the line continues across undefined points but breaks at null ones).
type SeriesArray = Array<number | null | undefined>;
/**
* Returns true if the given gap size exceeds the numeric spanGaps threshold
* of at least one series. Used to decide whether to insert a null break point.
*/
function gapExceedsThreshold(
gapSize: number,
seriesOptions: SeriesSpanGapsOption[],
): boolean {
return seriesOptions.some(
({ spanGaps }) =>
typeof spanGaps === 'number' && spanGaps > 0 && gapSize > spanGaps,
);
}
/**
* For each series with a numeric spanGaps threshold, insert a null data point
* between consecutive x timestamps whose gap exceeds the threshold.
*
* Why: uPlot draws a continuous line between all non-null points. When the
* time gap between two consecutive samples is larger than the configured
* spanGaps value, we inject a synthetic null at the midpoint so uPlot renders
* a visible break instead of a misleading straight line across the gap.
*
* Because uPlot's AlignedData shares a single x-axis across all series, a null
* is inserted for every series at each position where any series needs a break.
*
* Two-pass approach for performance:
* Pass 1 — count how many nulls will be inserted (no allocations).
* Pass 2 — fill pre-allocated output arrays by index (no push/reallocation).
*/
export function insertLargeGapNullsIntoAlignedData(
data: uPlot.AlignedData,
seriesOptions: SeriesSpanGapsOption[],
): uPlot.AlignedData {
const [xValues, ...seriesValues] = data;
if (
!Array.isArray(xValues) ||
xValues.length < 2 ||
seriesValues.length === 0
) {
return data;
}
const xs = xValues as number[];
const n = xs.length;
// Pass 1: count insertions needed so we know the exact output length.
// This lets us pre-allocate arrays rather than growing them dynamically.
let insertionCount = 0;
for (let i = 0; i < n - 1; i += 1) {
if (gapExceedsThreshold(xs[i + 1] - xs[i], seriesOptions)) {
insertionCount += 1;
}
}
// No gaps exceed any threshold — return the original data unchanged.
if (insertionCount === 0) {
return data;
}
// Pass 2: build output arrays of exact size and fill them.
// `out` is the write cursor into the output arrays.
const outputLen = n + insertionCount;
const newX = new Array<number>(outputLen);
const newSeries: SeriesArray[] = seriesValues.map(
() => new Array<number | null | undefined>(outputLen),
);
let out = 0;
for (let i = 0; i < n; i += 1) {
// Copy the real data point at position i
newX[out] = xs[i];
for (let s = 0; s < seriesValues.length; s += 1) {
newSeries[s][out] = (seriesValues[s] as SeriesArray)[i];
}
out += 1;
// If the gap to the next x timestamp exceeds the threshold, insert a
// synthetic null at the midpoint. The midpoint x is placed halfway
// between xs[i] and xs[i+1] (minimum 1 unit past xs[i] to stay unique).
if (i < n - 1 && gapExceedsThreshold(xs[i + 1] - xs[i], seriesOptions)) {
newX[out] = xs[i] + Math.max(1, Math.floor((xs[i + 1] - xs[i]) / 2));
for (let s = 0; s < seriesValues.length; s += 1) {
newSeries[s][out] = null; // null tells uPlot to break the line here
}
out += 1;
}
}
return [newX, ...newSeries] as uPlot.AlignedData;
}
/**
* Apply per-series spanGaps (boolean | number) handling to an aligned dataset.
*
* spanGaps controls how uPlot handles gaps in a series:
* - boolean true → convert null → undefined so uPlot spans over every gap
* (draws a continuous line, skipping missing samples)
* - boolean false → no change; nulls render as visible breaks (default)
* - number → insert a null break point between any two consecutive
* timestamps whose difference exceeds the threshold;
* gaps smaller than the threshold are left as-is
*
* The input data is expected to be of the form:
* [xValues, series1Values, series2Values, ...]
*/
export function applySpanGapsToAlignedData(
data: uPlot.AlignedData,
seriesOptions: SeriesSpanGapsOption[],
): uPlot.AlignedData {
const [xValues, ...seriesValues] = data;
if (!Array.isArray(xValues) || seriesValues.length === 0) {
return data;
}
// Numeric spanGaps: operates on the whole dataset at once because inserting
// null break points requires modifying the shared x-axis.
const hasNumericSpanGaps = seriesOptions.some(
({ spanGaps }) => typeof spanGaps === 'number',
);
const gapProcessed = hasNumericSpanGaps
? insertLargeGapNullsIntoAlignedData(data, seriesOptions)
: data;
// Boolean spanGaps === true: convert null → undefined per series so uPlot
// draws a continuous line across missing samples instead of breaking it.
// Skip this pass entirely if no series uses spanGaps: true.
const hasBooleanTrue = seriesOptions.some(({ spanGaps }) => spanGaps === true);
if (!hasBooleanTrue) {
return gapProcessed;
}
const [newX, ...newSeries] = gapProcessed;
const transformedSeries = newSeries.map((ys, idx) => {
const { spanGaps } = seriesOptions[idx] ?? {};
if (spanGaps !== true) {
// This series doesn't use spanGaps: true — leave it unchanged.
return ys;
}
// Replace null with undefined: uPlot skips undefined points without
// breaking the line, effectively spanning over the gap.
return (ys as SeriesArray).map((v) =>
v === null ? undefined : v,
) as uPlot.AlignedData[0];
});
return [newX, ...transformedSeries] as uPlot.AlignedData;
}

View File

@@ -1,90 +0,0 @@
import uPlot from 'uplot';
/**
* Scans outward from approxIdx to find the nearest non-null data index.
* posToIdx can land on a null when pixel density exceeds 1 point-per-pixel.
*/
export function findNearestNonNull(
yData: (number | null | undefined)[],
approxIdx: number,
): number {
for (let j = 1; j < 100; j++) {
if (yData[approxIdx + j] != null) {
return approxIdx + j;
}
if (yData[approxIdx - j] != null) {
return approxIdx - j;
}
}
return approxIdx;
}
/**
* Returns data indices of points sandwiched between two consecutive gaps that
* share a pixel boundary — meaning a point (or cluster) is isolated between them.
*/
export function findSandwichedIndices(
gaps: number[][],
yData: (number | null | undefined)[],
u: uPlot,
): number[] {
const indices: number[] = [];
for (let i = 0; i < gaps.length; i++) {
const nextGap = gaps[i + 1];
if (nextGap && gaps[i][1] === nextGap[0]) {
const approxIdx = u.posToIdx(gaps[i][1], true);
indices.push(
yData[approxIdx] == null ? findNearestNonNull(yData, approxIdx) : approxIdx,
);
}
}
return indices;
}
/**
* Points filter that shows data points isolated by gap-nulls (no connecting line).
* Used when spanGaps threshold mode injects nulls around gaps — without this,
* lone points become invisible because no line connects to them.
*
* Uses uPlot's gap pixel array rather than checking raw null neighbors in the
* data array. Returns an array of data indices (not a bitmask); null = no points.
*
*/
// eslint-disable-next-line max-params
export function isolatedPointFilter(
u: uPlot,
seriesIdx: number,
show: boolean,
gaps?: null | number[][],
): number[] | null {
if (show || !gaps || gaps.length === 0) {
return null;
}
const idxs = u.series[seriesIdx].idxs;
if (!idxs) {
return null;
}
const [firstIdx, lastIdx] = idxs;
const xData = u.data[0] as number[];
const yData = u.data[seriesIdx] as (number | null | undefined)[];
// valToPos with canvas=true matches the pixel space used by the gaps array.
const firstPos = Math.round(u.valToPos(xData[firstIdx], 'x', true));
const lastPos = Math.round(u.valToPos(xData[lastIdx], 'x', true));
const filtered: number[] = [];
if (gaps[0][0] === firstPos) {
filtered.push(firstIdx);
}
filtered.push(...findSandwichedIndices(gaps, yData, u));
if (gaps[gaps.length - 1][1] === lastPos) {
filtered.push(lastIdx);
}
return filtered.length ? filtered : null;
}

View File

@@ -1,8 +1,8 @@
import { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import { RoletypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
const orgId = '019ba2bb-2fa1-7b24-8159-cfca08617ef9';
export const managedRoles: AuthtypesRoleDTO[] = [
export const managedRoles: RoletypesRoleDTO[] = [
{
id: '019c24aa-2248-756f-9833-984f1ab63819',
createdAt: new Date('2026-02-03T18:00:55.624356Z'),
@@ -35,7 +35,7 @@ export const managedRoles: AuthtypesRoleDTO[] = [
},
];
export const customRoles: AuthtypesRoleDTO[] = [
export const customRoles: RoletypesRoleDTO[] = [
{
id: '019c24aa-3333-0001-aaaa-111111111111',
createdAt: new Date('2026-02-10T10:30:00.000Z'),
@@ -56,7 +56,7 @@ export const customRoles: AuthtypesRoleDTO[] = [
},
];
export const allRoles: AuthtypesRoleDTO[] = [...managedRoles, ...customRoles];
export const allRoles: RoletypesRoleDTO[] = [...managedRoles, ...customRoles];
export const listRolesSuccessResponse = {
status: 'success',

View File

@@ -141,7 +141,6 @@ export interface IBaseWidget {
showPoints?: boolean;
lineStyle?: LineStyle;
fillMode?: FillMode;
spanGaps?: boolean | number;
}
export interface Widgets extends IBaseWidget {
query: Query;

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/gorilla/mux"
)
@@ -15,7 +16,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Tags: []string{"role"},
Summary: "Create role",
Description: "This endpoint creates a role",
Request: new(authtypes.PostableRole),
Request: new(roletypes.PostableRole),
RequestContentType: "",
Response: new(types.Identifiable),
ResponseContentType: "application/json",
@@ -34,7 +35,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Description: "This endpoint lists all roles",
Request: nil,
RequestContentType: "",
Response: make([]*authtypes.Role, 0),
Response: make([]*roletypes.Role, 0),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
@@ -51,7 +52,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Description: "This endpoint gets a role",
Request: nil,
RequestContentType: "",
Response: new(authtypes.Role),
Response: new(roletypes.Role),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
@@ -83,7 +84,7 @@ func (provider *provider) addRoleRoutes(router *mux.Router) error {
Tags: []string{"role"},
Summary: "Patch role",
Description: "This endpoint patches a role",
Request: new(authtypes.PatchableRole),
Request: new(roletypes.PatchableRole),
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
@@ -29,10 +30,10 @@ type AuthZ interface {
ListObjects(context.Context, string, authtypes.Relation, authtypes.Typeable) ([]*authtypes.Object, error)
// Creates the role.
Create(context.Context, valuer.UUID, *authtypes.Role) error
Create(context.Context, valuer.UUID, *roletypes.Role) error
// Gets the role if it exists or creates one.
GetOrCreate(context.Context, valuer.UUID, *authtypes.Role) (*authtypes.Role, error)
GetOrCreate(context.Context, valuer.UUID, *roletypes.Role) (*roletypes.Role, error)
// Gets the objects associated with the given role and relation.
GetObjects(context.Context, valuer.UUID, valuer.UUID, authtypes.Relation) ([]*authtypes.Object, error)
@@ -41,7 +42,7 @@ type AuthZ interface {
GetResources(context.Context) []*authtypes.Resource
// Patches the role.
Patch(context.Context, valuer.UUID, *authtypes.Role) error
Patch(context.Context, valuer.UUID, *roletypes.Role) error
// Patches the objects in authorization server associated with the given role and relation
PatchObjects(context.Context, valuer.UUID, string, authtypes.Relation, []*authtypes.Object, []*authtypes.Object) error
@@ -50,19 +51,19 @@ type AuthZ interface {
Delete(context.Context, valuer.UUID, valuer.UUID) error
// Gets the role
Get(context.Context, valuer.UUID, valuer.UUID) (*authtypes.Role, error)
Get(context.Context, valuer.UUID, valuer.UUID) (*roletypes.Role, error)
// Gets the role by org_id and name
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*authtypes.Role, error)
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*roletypes.Role, error)
// Lists all the roles for the organization.
List(context.Context, valuer.UUID) ([]*authtypes.Role, error)
List(context.Context, valuer.UUID) ([]*roletypes.Role, error)
// Lists all the roles for the organization filtered by name
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*authtypes.Role, error)
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*roletypes.Role, error)
// Lists all the roles for the organization filtered by ids
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*authtypes.Role, error)
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*roletypes.Role, error)
// Grants a role to the subject based on role name.
Grant(context.Context, valuer.UUID, []string, string) error
@@ -74,7 +75,7 @@ type AuthZ interface {
ModifyGrant(context.Context, valuer.UUID, []string, []string, string) error
// Bootstrap the managed roles.
CreateManagedRoles(context.Context, valuer.UUID, []*authtypes.Role) error
CreateManagedRoles(context.Context, valuer.UUID, []*roletypes.Role) error
// Bootstrap managed roles transactions and user assignments
CreateManagedUserRoleTransactions(context.Context, valuer.UUID, valuer.UUID) error

View File

@@ -5,7 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
@@ -14,11 +14,11 @@ type store struct {
sqlstore sqlstore.SQLStore
}
func NewSqlAuthzStore(sqlstore sqlstore.SQLStore) authtypes.RoleStore {
func NewSqlAuthzStore(sqlstore sqlstore.SQLStore) roletypes.Store {
return &store{sqlstore: sqlstore}
}
func (store *store) Create(ctx context.Context, role *authtypes.StorableRole) error {
func (store *store) Create(ctx context.Context, role *roletypes.StorableRole) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
@@ -32,8 +32,8 @@ func (store *store) Create(ctx context.Context, role *authtypes.StorableRole) er
return nil
}
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.StorableRole, error) {
role := new(authtypes.StorableRole)
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.StorableRole, error) {
role := new(roletypes.StorableRole)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -43,14 +43,14 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
Where("id = ?", id).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, authtypes.ErrCodeRoleNotFound, "role with id: %s doesn't exist", id)
return nil, store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "role with id: %s doesn't exist", id)
}
return role, nil
}
func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.StorableRole, error) {
role := new(authtypes.StorableRole)
func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.StorableRole, error) {
role := new(roletypes.StorableRole)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -60,14 +60,14 @@ func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, na
Where("name = ?", name).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, authtypes.ErrCodeRoleNotFound, "role with name: %s doesn't exist", name)
return nil, store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "role with name: %s doesn't exist", name)
}
return role, nil
}
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.StorableRole, error) {
roles := make([]*authtypes.StorableRole, 0)
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -82,8 +82,8 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.S
return roles, nil
}
func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.StorableRole, error) {
roles := make([]*authtypes.StorableRole, 0)
func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -99,7 +99,7 @@ func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID,
if len(roles) != len(names) {
return nil, store.sqlstore.WrapNotFoundErrf(
nil,
authtypes.ErrCodeRoleNotFound,
roletypes.ErrCodeRoleNotFound,
"not all roles found for the provided names: %v", names,
)
}
@@ -107,8 +107,8 @@ func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID,
return roles, nil
}
func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.StorableRole, error) {
roles := make([]*authtypes.StorableRole, 0)
func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
@@ -124,7 +124,7 @@ func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, id
if len(roles) != len(ids) {
return nil, store.sqlstore.WrapNotFoundErrf(
nil,
authtypes.ErrCodeRoleNotFound,
roletypes.ErrCodeRoleNotFound,
"not all roles found for the provided ids: %v", ids,
)
}
@@ -132,7 +132,7 @@ func (store *store) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, id
return roles, nil
}
func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *authtypes.StorableRole) error {
func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *roletypes.StorableRole) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
@@ -153,12 +153,12 @@ func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUI
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(authtypes.StorableRole)).
Model(new(roletypes.StorableRole)).
Where("org_id = ?", orgID).
Where("id = ?", id).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, authtypes.ErrCodeRoleNotFound, "role with id %s doesn't exist", id)
return store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "role with id %s doesn't exist", id)
}
return nil

View File

@@ -8,6 +8,7 @@ import (
"github.com/SigNoz/signoz/pkg/authz/openfgaserver"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/factory"
@@ -18,7 +19,7 @@ import (
type provider struct {
server *openfgaserver.Server
store authtypes.RoleStore
store roletypes.Store
}
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile) factory.ProviderFactory[authz.AuthZ, authz.Config] {
@@ -67,61 +68,61 @@ func (provider *provider) ListObjects(ctx context.Context, subject string, relat
return provider.server.ListObjects(ctx, subject, relation, typeable)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) {
storableRole, err := provider.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
return authtypes.NewRoleFromStorableRole(storableRole), nil
return roletypes.NewRoleFromStorableRole(storableRole), nil
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.Role, error) {
storableRole, err := provider.store.GetByOrgIDAndName(ctx, orgID, name)
if err != nil {
return nil, err
}
return authtypes.NewRoleFromStorableRole(storableRole), nil
return roletypes.NewRoleFromStorableRole(storableRole), nil
}
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
storableRoles, err := provider.store.List(ctx, orgID)
if err != nil {
return nil, err
}
roles := make([]*authtypes.Role, len(storableRoles))
roles := make([]*roletypes.Role, len(storableRoles))
for idx, storableRole := range storableRoles {
roles[idx] = authtypes.NewRoleFromStorableRole(storableRole)
roles[idx] = roletypes.NewRoleFromStorableRole(storableRole)
}
return roles, nil
}
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.Role, error) {
storableRoles, err := provider.store.ListByOrgIDAndNames(ctx, orgID, names)
if err != nil {
return nil, err
}
roles := make([]*authtypes.Role, len(storableRoles))
roles := make([]*roletypes.Role, len(storableRoles))
for idx, storable := range storableRoles {
roles[idx] = authtypes.NewRoleFromStorableRole(storable)
roles[idx] = roletypes.NewRoleFromStorableRole(storable)
}
return roles, nil
}
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.Role, error) {
storableRoles, err := provider.store.ListByOrgIDAndIDs(ctx, orgID, ids)
if err != nil {
return nil, err
}
roles := make([]*authtypes.Role, len(storableRoles))
roles := make([]*roletypes.Role, len(storableRoles))
for idx, storable := range storableRoles {
roles[idx] = authtypes.NewRoleFromStorableRole(storable)
roles[idx] = roletypes.NewRoleFromStorableRole(storable)
}
return roles, nil
@@ -178,10 +179,10 @@ func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names [
return provider.Write(ctx, nil, tuples)
}
func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID, managedRoles []*authtypes.Role) error {
func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID, managedRoles []*roletypes.Role) error {
err := provider.store.RunInTx(ctx, func(ctx context.Context) error {
for _, role := range managedRoles {
err := provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
err := provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
if err != nil {
return err
}
@@ -198,15 +199,15 @@ func (provider *provider) CreateManagedRoles(ctx context.Context, _ valuer.UUID,
}
func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context, orgID valuer.UUID, userID valuer.UUID) error {
return provider.Grant(ctx, orgID, []string{authtypes.SigNozAdminRoleName}, authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil))
return provider.Grant(ctx, orgID, []string{roletypes.SigNozAdminRoleName}, authtypes.MustNewSubject(authtypes.TypeableUser, userID.String(), orgID, nil))
}
func (setter *provider) Create(_ context.Context, _ valuer.UUID, _ *authtypes.Role) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
func (setter *provider) Create(_ context.Context, _ valuer.UUID, _ *roletypes.Role) error {
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) GetOrCreate(_ context.Context, _ valuer.UUID, _ *authtypes.Role) (*authtypes.Role, error) {
return nil, errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
func (provider *provider) GetOrCreate(_ context.Context, _ valuer.UUID, _ *roletypes.Role) (*roletypes.Role, error) {
return nil, errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource {
@@ -214,19 +215,19 @@ func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource
}
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) {
return nil, errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
return nil, errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) Patch(_ context.Context, _ valuer.UUID, _ *authtypes.Role) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
func (provider *provider) Patch(_ context.Context, _ valuer.UUID, _ *roletypes.Role) error {
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) PatchObjects(_ context.Context, _ valuer.UUID, _ string, _ authtypes.Relation, _, _ []*authtypes.Object) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) Delete(_ context.Context, _ valuer.UUID, _ valuer.UUID) error {
return errors.Newf(errors.TypeUnsupported, authtypes.ErrCodeRoleUnsupported, "not implemented")
return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented")
}
func (provider *provider) MustGetTypeables() []authtypes.Typeable {

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -29,13 +30,13 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
return
}
req := new(authtypes.PostableRole)
req := new(roletypes.PostableRole)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
role := authtypes.NewRole(req.Name, req.Description, authtypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID))
role := roletypes.NewRole(req.Name, req.Description, roletypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID))
err = handler.authz.Create(ctx, valuer.MustNewUUID(claims.OrgID), role)
if err != nil {
render.Error(rw, err)
@@ -55,7 +56,7 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
id, ok := mux.Vars(r)["id"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
return
}
roleID, err := valuer.NewUUID(id)
@@ -83,7 +84,7 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
id, ok := mux.Vars(r)["id"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
return
}
roleID, err := valuer.NewUUID(id)
@@ -94,7 +95,7 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
relationStr, ok := mux.Vars(r)["relation"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, authtypes.ErrCodeRoleInvalidInput, "relation is missing from the request"))
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "relation is missing from the request"))
return
}
relation, err := authtypes.NewRelation(relationStr)
@@ -149,7 +150,7 @@ func (handler *handler) Patch(rw http.ResponseWriter, r *http.Request) {
return
}
req := new(authtypes.PatchableRole)
req := new(roletypes.PatchableRole)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return

View File

@@ -10,6 +10,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -55,9 +56,9 @@ func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc {
}
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozEditorRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozViewerRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozEditorRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozViewerRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
@@ -107,8 +108,8 @@ func (middleware *AuthZ) EditAccess(next http.HandlerFunc) http.HandlerFunc {
}
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozEditorRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozEditorRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(
@@ -158,7 +159,7 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc {
}
selectors := []authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
}
err = middleware.authzService.CheckWithTupleCreation(

View File

@@ -19,6 +19,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/emailtypes"
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/dustin/go-humanize"
)
@@ -262,7 +263,7 @@ func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ..
createUserOpts := root.NewCreateUserOptions(opts...)
// since assign is idempotant multiple calls to assign won't cause issues in case of retries.
err := module.authz.Grant(ctx, input.OrgID, []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(input.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.StringValue(), input.OrgID, nil))
err := module.authz.Grant(ctx, input.OrgID, []string{roletypes.MustGetSigNozManagedRoleFromExistingRole(input.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.StringValue(), input.OrgID, nil))
if err != nil {
return err
}
@@ -332,8 +333,8 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
if user.Role != "" && user.Role != existingUser.Role {
err = m.authz.ModifyGrant(ctx,
orgID,
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role)},
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role)},
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil),
)
if err != nil {
@@ -394,7 +395,7 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
}
// since revoke is idempotant multiple calls to revoke won't cause issues in case of retries
err = module.authz.Revoke(ctx, orgID, []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil))
err = module.authz.Revoke(ctx, orgID, []string{roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil))
if err != nil {
return err
}
@@ -557,7 +558,7 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
if err = module.authz.Grant(
ctx,
user.OrgID,
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
); err != nil {
return err
@@ -691,7 +692,7 @@ func (module *Module) CreateFirstUser(ctx context.Context, organization *types.O
return nil, err
}
managedRoles := authtypes.NewManagedRoles(organization.ID)
managedRoles := roletypes.NewManagedRoles(organization.ID)
err = module.authz.CreateManagedUserRoleTransactions(ctx, organization.ID, user.ID)
if err != nil {
return nil, err
@@ -792,7 +793,7 @@ func (module *Module) activatePendingUser(ctx context.Context, user *types.User)
err := module.authz.Grant(
ctx,
user.OrgID,
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
)
if err != nil {

View File

@@ -11,6 +11,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -158,8 +159,8 @@ func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID
if oldRole != types.RoleAdmin {
if err := s.authz.ModifyGrant(ctx,
orgID,
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(oldRole)},
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(types.RoleAdmin)},
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(oldRole)},
[]string{roletypes.MustGetSigNozManagedRoleFromExistingRole(types.RoleAdmin)},
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), orgID, nil),
); err != nil {
return err

View File

@@ -20,7 +20,6 @@ import (
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/dustin/go-humanize"
"golang.org/x/exp/maps"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
@@ -159,8 +158,7 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
metricNames := make([]string, 0)
for idx, query := range req.CompositeQuery.Queries {
event.QueryType = query.Type.StringValue()
switch query.Type {
case qbtypes.QueryTypeBuilder:
if query.Type == qbtypes.QueryTypeBuilder {
if spec, ok := query.Spec.(qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]); ok {
for _, agg := range spec.Aggregations {
if agg.MetricName != "" {
@@ -238,7 +236,7 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
}
req.CompositeQuery.Queries[idx].Spec = spec
}
case qbtypes.QueryTypePromQL:
} else if query.Type == qbtypes.QueryTypePromQL {
event.MetricsUsed = true
switch spec := query.Spec.(type) {
case qbtypes.PromQuery:
@@ -249,7 +247,7 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
}
req.CompositeQuery.Queries[idx].Spec = spec
}
case qbtypes.QueryTypeClickHouseSQL:
} else if query.Type == qbtypes.QueryTypeClickHouseSQL {
switch spec := query.Spec.(type) {
case qbtypes.ClickHouseQuery:
if strings.TrimSpace(spec.Query) != "" {
@@ -258,7 +256,7 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
event.TracesUsed = strings.Contains(spec.Query, "signoz_traces")
}
}
case qbtypes.QueryTypeTraceOperator:
} else if query.Type == qbtypes.QueryTypeTraceOperator {
if spec, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator); ok {
if spec.StepInterval.Seconds() == 0 {
spec.StepInterval = qbtypes.Step{
@@ -278,9 +276,23 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
}
}
// Fetch temporality for all metrics at once
var metricTemporality map[string]metrictypes.Temporality
var metricTypes map[string]metrictypes.Type
if len(metricNames) > 0 {
var err error
metricTemporality, metricTypes, err = q.metadataStore.FetchTemporalityAndTypeMulti(ctx, req.Start, req.End, metricNames...)
if err != nil {
q.logger.WarnContext(ctx, "failed to fetch metric temporality", "error", err, "metrics", metricNames)
// Continue without temporality - statement builder will handle unspecified
metricTemporality = make(map[string]metrictypes.Temporality)
metricTypes = make(map[string]metrictypes.Type)
}
q.logger.DebugContext(ctx, "fetched metric temporalities and types", "metric_temporality", metricTemporality, "metric_types", metricTypes)
}
queries := make(map[string]qbtypes.Query)
steps := make(map[string]qbtypes.Step)
missingMetrics := []string{}
for _, query := range req.CompositeQuery.Queries {
var queryName string
@@ -362,26 +374,15 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
queries[spec.Name] = bq
steps[spec.Name] = spec.StepInterval
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
var metricTemporality map[string]metrictypes.Temporality
var metricTypes map[string]metrictypes.Type
if len(metricNames) > 0 {
var err error
metricTemporality, metricTypes, err = q.metadataStore.FetchTemporalityAndTypeMulti(ctx, req.Start, req.End, metricNames...)
if err != nil {
q.logger.WarnContext(ctx, "failed to fetch metric temporality", "error", err, "metrics", metricNames)
return nil, errors.NewInternalf(errors.CodeInternal, "failed to fetch metrics temporality")
}
q.logger.DebugContext(ctx, "fetched metric temporalities and types", "metric_temporality", metricTemporality, "metric_types", metricTypes)
}
for i := range spec.Aggregations {
if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Temporality == metrictypes.Unknown {
if temp, ok := metricTemporality[spec.Aggregations[i].MetricName]; ok && temp != metrictypes.Unknown {
spec.Aggregations[i].Temporality = temp
}
}
// TODO(srikanthccv): warn when the metric is missing
if spec.Aggregations[i].Temporality == metrictypes.Unknown {
missingMetrics = append(missingMetrics, spec.Aggregations[i].MetricName)
continue
spec.Aggregations[i].Temporality = metrictypes.Unspecified
}
if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Type == metrictypes.UnspecifiedType {
@@ -408,24 +409,6 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
}
}
}
if len(missingMetrics) > 0 {
lastSeenInfo, _ := q.metadataStore.FetchLastSeenInfoMulti(ctx, missingMetrics...)
lastSeenStr := func(name string) string {
if ts, ok := lastSeenInfo[name]; ok && ts > 0 {
ago := humanize.RelTime(time.UnixMilli(ts), time.Now(), "ago", "from now")
return fmt.Sprintf("%s (last seen %s)", name, ago)
}
return name
}
if len(missingMetrics) == 1 {
return nil, errors.NewNotFoundf(errors.CodeNotFound, "no data found for the metric %s in the query time range", lastSeenStr(missingMetrics[0]))
}
parts := make([]string, len(missingMetrics))
for i, m := range missingMetrics {
parts[i] = lastSeenStr(m)
}
return nil, errors.NewNotFoundf(errors.CodeNotFound, "no data found for the following metrics in the query time range: %s", strings.Join(parts, ", "))
}
qbResp, qbErr := q.run(ctx, orgID, queries, req, steps, event)
if qbResp != nil {
qbResp.QBEvent = event
@@ -680,7 +663,7 @@ func (q *querier) run(
}
// executeWithCache executes a query using the bucket cache
func (q *querier) executeWithCache(ctx context.Context, orgID valuer.UUID, query qbtypes.Query, step qbtypes.Step, _ bool) (*qbtypes.Result, error) {
func (q *querier) executeWithCache(ctx context.Context, orgID valuer.UUID, query qbtypes.Query, step qbtypes.Step, noCache bool) (*qbtypes.Result, error) {
// Get cached data and missing ranges
cachedResult, missingRanges := q.bucketCache.GetMissRanges(ctx, orgID, query, step)

View File

@@ -76,21 +76,6 @@ func TestManager_TestNotification_SendUnmatched_ThresholdRule(t *testing.T) {
alertDataRows := cmock.NewRows(cols, tc.Values)
mock := mockStore.Mock()
// Mock metadata queries for FetchTemporalityAndTypeMulti
// First query: fetchMetricsTemporalityAndType (from signoz_metrics time series table)
metadataCols := []cmock.ColumnType{
{Name: "metric_name", Type: "String"},
{Name: "temporality", Type: "String"},
{Name: "type", Type: "String"},
{Name: "is_monotonic", Type: "Bool"},
}
metadataRows := cmock.NewRows(metadataCols, [][]any{
{"probe_success", metrictypes.Unspecified, metrictypes.GaugeType, false},
})
mock.ExpectQuery("*distributed_time_series_v4*").WithArgs(nil, nil, nil).WillReturnRows(metadataRows)
// Second query: fetchMeterSourceMetricsTemporalityAndType (from signoz_meter table)
emptyMetadataRows := cmock.NewRows(metadataCols, [][]any{})
mock.ExpectQuery("*meter*").WithArgs(nil).WillReturnRows(emptyMetadataRows)
// Generate query arguments for the metric query
evalTime := time.Now().UTC()

View File

@@ -7,7 +7,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
@@ -54,7 +54,7 @@ func (migration *addManagedRoles) Up(ctx context.Context, db *bun.DB) error {
return err
}
managedRoles := []*authtypes.StorableRole{}
managedRoles := []*roletypes.StorableRole{}
for _, orgIDStr := range orgIDs {
orgID, err := valuer.NewUUID(orgIDStr)
if err != nil {
@@ -62,20 +62,20 @@ func (migration *addManagedRoles) Up(ctx context.Context, db *bun.DB) error {
}
// signoz admin
signozAdminRole := authtypes.NewRole(authtypes.SigNozAdminRoleName, authtypes.SigNozAdminRoleDescription, authtypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, authtypes.NewStorableRoleFromRole(signozAdminRole))
signozAdminRole := roletypes.NewRole(roletypes.SigNozAdminRoleName, roletypes.SigNozAdminRoleDescription, roletypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, roletypes.NewStorableRoleFromRole(signozAdminRole))
// signoz editor
signozEditorRole := authtypes.NewRole(authtypes.SigNozEditorRoleName, authtypes.SigNozEditorRoleDescription, authtypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, authtypes.NewStorableRoleFromRole(signozEditorRole))
signozEditorRole := roletypes.NewRole(roletypes.SigNozEditorRoleName, roletypes.SigNozEditorRoleDescription, roletypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, roletypes.NewStorableRoleFromRole(signozEditorRole))
// signoz viewer
signozViewerRole := authtypes.NewRole(authtypes.SigNozViewerRoleName, authtypes.SigNozViewerRoleDescription, authtypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, authtypes.NewStorableRoleFromRole(signozViewerRole))
signozViewerRole := roletypes.NewRole(roletypes.SigNozViewerRoleName, roletypes.SigNozViewerRoleDescription, roletypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, roletypes.NewStorableRoleFromRole(signozViewerRole))
// signoz anonymous
signozAnonymousRole := authtypes.NewRole(authtypes.SigNozAnonymousRoleName, authtypes.SigNozAnonymousRoleDescription, authtypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, authtypes.NewStorableRoleFromRole(signozAnonymousRole))
signozAnonymousRole := roletypes.NewRole(roletypes.SigNozAnonymousRoleName, roletypes.SigNozAnonymousRoleDescription, roletypes.RoleTypeManaged, orgID)
managedRoles = append(managedRoles, roletypes.NewStorableRoleFromRole(signozAnonymousRole))
}
if len(managedRoles) > 0 {

View File

@@ -6,7 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/oklog/ulid/v2"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
@@ -83,7 +83,7 @@ func (migration *addAnonymousPublicDashboardTransaction) Up(ctx context.Context,
INSERT INTO tuple (store, object_type, object_id, relation, _user, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, _user) DO NOTHING`,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role:organization/"+orgID+"/role/"+authtypes.SigNozAnonymousRoleName+"#assignee", "userset", tupleID, now,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role:organization/"+orgID+"/role/"+roletypes.SigNozAnonymousRoleName+"#assignee", "userset", tupleID, now,
)
if err != nil {
return err
@@ -102,7 +102,7 @@ func (migration *addAnonymousPublicDashboardTransaction) Up(ctx context.Context,
INSERT INTO changelog (store, object_type, object_id, relation, _user, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role:organization/"+orgID+"/role/"+authtypes.SigNozAnonymousRoleName+"#assignee", "TUPLE_OPERATION_WRITE", tupleID, now,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role:organization/"+orgID+"/role/"+roletypes.SigNozAnonymousRoleName+"#assignee", "TUPLE_OPERATION_WRITE", tupleID, now,
)
if err != nil {
return err
@@ -113,7 +113,7 @@ func (migration *addAnonymousPublicDashboardTransaction) Up(ctx context.Context,
INSERT INTO tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, user_type, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) DO NOTHING`,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role", "organization/"+orgID+"/role/"+authtypes.SigNozAnonymousRoleName, "assignee", "userset", tupleID, now,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role", "organization/"+orgID+"/role/"+roletypes.SigNozAnonymousRoleName, "assignee", "userset", tupleID, now,
)
if err != nil {
return err
@@ -132,7 +132,7 @@ func (migration *addAnonymousPublicDashboardTransaction) Up(ctx context.Context,
INSERT INTO changelog (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation, operation, ulid, inserted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (store, ulid, object_type) DO NOTHING`,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role", "organization/"+orgID+"/role/"+authtypes.SigNozAnonymousRoleName, "assignee", 0, tupleID, now,
storeID, "metaresource", "organization/"+orgID+"/public-dashboard/*", "read", "role", "organization/"+orgID+"/role/"+roletypes.SigNozAnonymousRoleName, "assignee", 0, tupleID, now,
)
if err != nil {
return err

View File

@@ -1928,37 +1928,3 @@ func (t *telemetryMetaStore) GetFirstSeenFromMetricMetadata(ctx context.Context,
return result, nil
}
func (t *telemetryMetaStore) FetchLastSeenInfoMulti(ctx context.Context, metricNames ...string) (map[string]int64, error) {
sb := sqlbuilder.Select(
"metric_name",
"max(unix_milli)",
).
From(t.metricsDBName + "." + telemetrymetrics.TimeseriesV4TableName)
sb.Where(sb.In("metric_name", metricNames))
sb.GroupBy("metric_name")
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
t.logger.DebugContext(ctx, "fetching metric last seen timestamp", "query", query, "args", args)
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch metric last seen info")
}
defer rows.Close()
lastSeenInfo := make(map[string]int64)
for rows.Next() {
var metricName string
var unix_milli int64
if err := rows.Scan(&metricName, &unix_milli); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan last seen info result")
}
lastSeenInfo[metricName] = unix_milli
}
if err := rows.Err(); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error iterating over metrics temporality rows")
}
return lastSeenInfo, nil
}

View File

@@ -1,13 +1,13 @@
package authtypes
package roletypes
import (
"context"
"encoding/json"
"regexp"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"github.com/uptrace/bun"
@@ -51,7 +51,7 @@ var (
)
var (
TypeableResourcesRoles = MustNewTypeableMetaResources(MustNewName("roles"))
TypeableResourcesRoles = authtypes.MustNewTypeableMetaResources(authtypes.MustNewName("roles"))
)
type StorableRole struct {
@@ -194,20 +194,20 @@ func (role *PatchableRole) UnmarshalJSON(data []byte) error {
return nil
}
func GetAdditionTuples(name string, orgID valuer.UUID, relation Relation, additions []*Object) ([]*openfgav1.TupleKey, error) {
func GetAdditionTuples(name string, orgID valuer.UUID, relation authtypes.Relation, additions []*authtypes.Object) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, object := range additions {
typeable := MustNewTypeableFromType(object.Resource.Type, object.Resource.Name)
typeable := authtypes.MustNewTypeableFromType(object.Resource.Type, object.Resource.Name)
transactionTuples, err := typeable.Tuples(
MustNewSubject(
TypeableRole,
authtypes.MustNewSubject(
authtypes.TypeableRole,
name,
orgID,
&RelationAssignee,
&authtypes.RelationAssignee,
),
relation,
[]Selector{object.Selector},
[]authtypes.Selector{object.Selector},
orgID,
)
if err != nil {
@@ -220,20 +220,20 @@ func GetAdditionTuples(name string, orgID valuer.UUID, relation Relation, additi
return tuples, nil
}
func GetDeletionTuples(name string, orgID valuer.UUID, relation Relation, deletions []*Object) ([]*openfgav1.TupleKey, error) {
func GetDeletionTuples(name string, orgID valuer.UUID, relation authtypes.Relation, deletions []*authtypes.Object) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, object := range deletions {
typeable := MustNewTypeableFromType(object.Resource.Type, object.Resource.Name)
typeable := authtypes.MustNewTypeableFromType(object.Resource.Type, object.Resource.Name)
transactionTuples, err := typeable.Tuples(
MustNewSubject(
TypeableRole,
authtypes.MustNewSubject(
authtypes.TypeableRole,
name,
orgID,
&RelationAssignee,
&authtypes.RelationAssignee,
),
relation,
[]Selector{object.Selector},
[]authtypes.Selector{object.Selector},
orgID,
)
if err != nil {
@@ -254,15 +254,3 @@ func MustGetSigNozManagedRoleFromExistingRole(role types.Role) string {
return managedRole
}
type RoleStore interface {
Create(context.Context, *StorableRole) error
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableRole, error)
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*StorableRole, error)
List(context.Context, valuer.UUID) ([]*StorableRole, error)
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*StorableRole, error)
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*StorableRole, error)
Update(context.Context, valuer.UUID, *StorableRole) error
Delete(context.Context, valuer.UUID, valuer.UUID) error
RunInTx(context.Context, func(ctx context.Context) error) error
}

View File

@@ -0,0 +1,19 @@
package roletypes
import (
"context"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
Create(context.Context, *StorableRole) error
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableRole, error)
GetByOrgIDAndName(context.Context, valuer.UUID, string) (*StorableRole, error)
List(context.Context, valuer.UUID) ([]*StorableRole, error)
ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*StorableRole, error)
ListByOrgIDAndIDs(context.Context, valuer.UUID, []valuer.UUID) ([]*StorableRole, error)
Update(context.Context, valuer.UUID, *StorableRole) error
Delete(context.Context, valuer.UUID, valuer.UUID) error
RunInTx(context.Context, func(ctx context.Context) error) error
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
@@ -102,10 +102,10 @@ func NewServiceAccountFromStorables(storableServiceAccount *StorableServiceAccou
}
}
func NewServiceAccountsFromRoles(storableServiceAccounts []*StorableServiceAccount, roles []*authtypes.Role, serviceAccountIDToRoleIDsMap map[string][]valuer.UUID) []*ServiceAccount {
func NewServiceAccountsFromRoles(storableServiceAccounts []*StorableServiceAccount, roles []*roletypes.Role, serviceAccountIDToRoleIDsMap map[string][]valuer.UUID) []*ServiceAccount {
serviceAccounts := make([]*ServiceAccount, 0, len(storableServiceAccounts))
roleIDToRole := make(map[string]*authtypes.Role, len(roles))
roleIDToRole := make(map[string]*roletypes.Role, len(roles))
for _, role := range roles {
roleIDToRole[role.ID.String()] = role
}

View File

@@ -5,7 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
@@ -19,7 +19,7 @@ type StorableServiceAccountRole struct {
RoleID string `bun:"role_id"`
}
func NewStorableServiceAccountRoles(serviceAccountID valuer.UUID, roles []*authtypes.Role) []*StorableServiceAccountRole {
func NewStorableServiceAccountRoles(serviceAccountID valuer.UUID, roles []*roletypes.Role) []*StorableServiceAccountRole {
storableServiceAccountRoles := make([]*StorableServiceAccountRole, len(roles))
for idx, role := range roles {
storableServiceAccountRoles[idx] = &StorableServiceAccountRole{
@@ -38,7 +38,7 @@ func NewStorableServiceAccountRoles(serviceAccountID valuer.UUID, roles []*autht
return storableServiceAccountRoles
}
func NewRolesFromStorableServiceAccountRoles(storable []*StorableServiceAccountRole, roles []*authtypes.Role) ([]string, error) {
func NewRolesFromStorableServiceAccountRoles(storable []*StorableServiceAccountRole, roles []*roletypes.Role) ([]string, error) {
roleIDToName := make(map[string]string, len(roles))
for _, role := range roles {
roleIDToName[role.ID.String()] = role.Name

View File

@@ -45,8 +45,6 @@ type MetadataStore interface {
// GetFirstSeenFromMetricMetadata gets the first seen timestamp for a metric metadata lookup key.
GetFirstSeenFromMetricMetadata(ctx context.Context, lookupKeys []MetricMetadataLookupKey) (map[MetricMetadataLookupKey]int64, error)
FetchLastSeenInfoMulti(ctx context.Context, metricNames ...string) (map[string]int64, error)
}
type MetricMetadataLookupKey struct {

View File

@@ -342,7 +342,3 @@ func (m *MockMetadataStore) SetFirstSeenFromMetricMetadata(firstSeenMap map[tele
m.LookupKeysMap[key] = value
}
}
func (m *MockMetadataStore) FetchLastSeenInfoMulti(ctx context.Context, metricNames ...string) (map[string]int64, error) {
return make(map[string]int64), nil
}

View File

@@ -1,4 +1,3 @@
from datetime import datetime, timedelta, timezone
from http import HTTPStatus
from typing import Callable, List
@@ -8,7 +7,6 @@ from sqlalchemy import sql
from wiremock.resources.mappings import Mapping
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
from fixtures.metrics import Metrics
from fixtures.types import Operation, SigNoz, TestContainerDocker
@@ -76,37 +74,9 @@ def test_public_dashboard_widget_query_range(
signoz: SigNoz,
create_user_admin: Operation, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
):
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
# Insert metric data so the widget query returns results instead of 404
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
metrics: List[Metrics] = [
Metrics(
metric_name="container.cpu.time",
labels={"service": "test-service"},
timestamp=now - timedelta(minutes=5),
value=100.0,
temporality="Cumulative",
),
Metrics(
metric_name="container.cpu.time",
labels={"service": "test-service"},
timestamp=now - timedelta(minutes=3),
value=200.0,
temporality="Cumulative",
),
Metrics(
metric_name="container.cpu.time",
labels={"service": "test-service"},
timestamp=now - timedelta(minutes=1),
value=300.0,
temporality="Cumulative",
),
]
insert_metrics(metrics)
dashboard_req = {
"title": "Test Widget Query Range Dashboard",
"description": "For testing widget query range",

View File

@@ -10,16 +10,12 @@ from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
from fixtures.metrics import Metrics
from fixtures.querier import (
assert_minutely_bucket_values,
build_builder_query,
find_named_result,
index_series_by_label,
make_query_request,
)
from fixtures.utils import get_testdata_file_path
FILL_GAPS = "fillGaps"
FILL_ZERO = "fillZero"
HISTOGRAM_FILE = get_testdata_file_path("histogram_data_1h.jsonl")
def _build_format_options(fill_mode: str) -> Dict[str, Any]:
@@ -584,39 +580,3 @@ def test_metrics_fill_formula_with_group_by(
expected_by_ts=expectations[group],
context=f"metrics/{fill_mode}/F1/{group}",
)
def test_histogram_p90_returns_404_outside_data_window(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_metrics: Callable[[List[Metrics]], None],
) -> None:
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
metric_name = "test_p90_last_seen_bucket"
metrics = Metrics.load_from_file(
HISTOGRAM_FILE,
base_time=now - timedelta(minutes=90),
metric_name_override=metric_name,
)
insert_metrics(metrics)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
query = build_builder_query(
"A",
metric_name,
"doesnotreallymatter",
"p90",
)
end_ms = int(now.timestamp() * 1000)
start_2h = int((now - timedelta(hours=2)).timestamp() * 1000)
response = make_query_request(signoz, token, start_2h, end_ms, [query])
assert response.status_code == HTTPStatus.OK
assert response.json()["status"] == "success"
start_15m = int((now - timedelta(minutes=15)).timestamp() * 1000)
response = make_query_request(signoz, token, start_15m, end_ms, [query])
assert response.status_code == HTTPStatus.NOT_FOUND