mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-19 19:22:17 +00:00
Compare commits
1 Commits
refactor/s
...
fix/order-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
099e7a98c5 |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -49,7 +49,6 @@ jobs:
|
||||
- ttl
|
||||
- alerts
|
||||
- ingestionkeys
|
||||
- rootuser
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
- sqlite
|
||||
|
||||
@@ -328,18 +328,15 @@ user:
|
||||
##################### IdentN #####################
|
||||
identn:
|
||||
tokenizer:
|
||||
# toggle tokenizer identN
|
||||
# toggle the identN resolver
|
||||
enabled: true
|
||||
# headers to use for tokenizer identN resolver
|
||||
headers:
|
||||
- Authorization
|
||||
- Sec-WebSocket-Protocol
|
||||
apikey:
|
||||
# toggle apikey identN
|
||||
# toggle the identN resolver
|
||||
enabled: true
|
||||
# headers to use for apikey identN resolver
|
||||
headers:
|
||||
- SIGNOZ-API-KEY
|
||||
impersonation:
|
||||
# toggle impersonation identN, when enabled, all requests will impersonate the root user
|
||||
enabled: false
|
||||
|
||||
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.116.1
|
||||
image: signoz/signoz:v0.116.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.116.1
|
||||
image: signoz/signoz:v0.116.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.116.1}
|
||||
image: signoz/signoz:${VERSION:-v0.116.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.116.1}
|
||||
image: signoz/signoz:${VERSION:-v0.116.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -598,39 +598,6 @@ components:
|
||||
required:
|
||||
- config
|
||||
type: object
|
||||
GlobaltypesAPIKeyConfig:
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
type: object
|
||||
GlobaltypesConfig:
|
||||
properties:
|
||||
external_url:
|
||||
type: string
|
||||
identN:
|
||||
$ref: '#/components/schemas/GlobaltypesIdentNConfig'
|
||||
ingestion_url:
|
||||
type: string
|
||||
type: object
|
||||
GlobaltypesIdentNConfig:
|
||||
properties:
|
||||
apikey:
|
||||
$ref: '#/components/schemas/GlobaltypesAPIKeyConfig'
|
||||
impersonation:
|
||||
$ref: '#/components/schemas/GlobaltypesImpersonationConfig'
|
||||
tokenizer:
|
||||
$ref: '#/components/schemas/GlobaltypesTokenizerConfig'
|
||||
type: object
|
||||
GlobaltypesImpersonationConfig:
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
type: object
|
||||
GlobaltypesTokenizerConfig:
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
type: object
|
||||
MetricsexplorertypesListMetric:
|
||||
properties:
|
||||
description:
|
||||
@@ -2063,6 +2030,13 @@ components:
|
||||
required:
|
||||
- id
|
||||
type: object
|
||||
TypesGettableGlobalConfig:
|
||||
properties:
|
||||
external_url:
|
||||
type: string
|
||||
ingestion_url:
|
||||
type: string
|
||||
type: object
|
||||
TypesIdentifiable:
|
||||
properties:
|
||||
id:
|
||||
@@ -2087,11 +2061,6 @@ components:
|
||||
type: string
|
||||
role:
|
||||
type: string
|
||||
roles:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
token:
|
||||
type: string
|
||||
updatedAt:
|
||||
@@ -2174,11 +2143,6 @@ components:
|
||||
type: string
|
||||
role:
|
||||
type: string
|
||||
roles:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
TypesPostableResetPassword:
|
||||
properties:
|
||||
@@ -2245,11 +2209,6 @@ components:
|
||||
type: string
|
||||
role:
|
||||
type: string
|
||||
roles:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
updatedAt:
|
||||
@@ -3296,7 +3255,7 @@ paths:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/GlobaltypesConfig'
|
||||
$ref: '#/components/schemas/TypesGettableGlobalConfig'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
@@ -3304,12 +3263,29 @@ paths:
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
summary: Get global config
|
||||
tags:
|
||||
- global
|
||||
@@ -5838,9 +5814,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
summary: Get ingestion keys for workspace
|
||||
tags:
|
||||
- gateway
|
||||
@@ -5888,9 +5864,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
summary: Create ingestion key for workspace
|
||||
tags:
|
||||
- gateway
|
||||
@@ -5928,9 +5904,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
summary: Delete ingestion key for workspace
|
||||
tags:
|
||||
- gateway
|
||||
@@ -5972,9 +5948,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
summary: Update ingestion key for workspace
|
||||
tags:
|
||||
- gateway
|
||||
@@ -6029,9 +6005,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
summary: Create limit for the ingestion key
|
||||
tags:
|
||||
- gateway
|
||||
@@ -6069,9 +6045,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
summary: Delete limit for the ingestion key
|
||||
tags:
|
||||
- gateway
|
||||
@@ -6113,9 +6089,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
summary: Update limit for the ingestion key
|
||||
tags:
|
||||
- gateway
|
||||
@@ -6173,9 +6149,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- EDITOR
|
||||
- ADMIN
|
||||
summary: Search ingestion keys for workspace
|
||||
tags:
|
||||
- gateway
|
||||
|
||||
@@ -198,10 +198,7 @@ func (provider *provider) Checkout(ctx context.Context, organizationID valuer.UU
|
||||
|
||||
response, err := provider.zeus.GetCheckoutURL(ctx, activeLicense.Key, body)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeAlreadyExists) {
|
||||
return nil, errors.WithAdditionalf(err, "checkout has already been completed for this account. Please click 'Refresh Status' to sync your subscription")
|
||||
}
|
||||
return nil, err
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to generate checkout session")
|
||||
}
|
||||
|
||||
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
|
||||
@@ -220,7 +217,7 @@ func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID
|
||||
|
||||
response, err := provider.zeus.GetPortalURL(ctx, activeLicense.Key, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to generate portal session")
|
||||
}
|
||||
|
||||
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"log/slog"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
@@ -20,6 +18,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type CloudIntegrationConnectionParamsResponse struct {
|
||||
@@ -170,7 +169,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
|
||||
|
||||
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, []string{authtypes.SigNozViewerRoleName}, valuer.MustNewUUID(orgId), types.UserStatusActive)
|
||||
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId), types.UserStatusActive)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func (provider *provider) WrapNotFoundErrf(err error, code errors.Code, format s
|
||||
|
||||
func (provider *provider) WrapAlreadyExistsErrf(err error, code errors.Code, format string, args ...any) error {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) && (pgErr.Code == "23505" || pgErr.Code == "23503") {
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||
return errors.Wrapf(err, errors.TypeAlreadyExists, code, format, args...)
|
||||
}
|
||||
|
||||
|
||||
@@ -193,16 +193,6 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector:
|
||||
// TODO: Make this generic on removal of redux
|
||||
"CallExpression[callee.property.name='getState'][callee.object.name=/^use/]",
|
||||
message:
|
||||
'Avoid calling .getState() directly. Export a standalone action from the store instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@@ -227,13 +217,5 @@ module.exports = {
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Store definition files are the only place .getState() is permitted —
|
||||
// they are the canonical source for standalone action exports.
|
||||
files: ['**/*Store.{ts,tsx}'],
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -24,8 +24,7 @@ const config: Config.InitialOptions = {
|
||||
'<rootDir>/node_modules/@signozhq/icons/dist/index.esm.js',
|
||||
'^react-syntax-highlighter/dist/esm/(.*)$':
|
||||
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',
|
||||
'^@signozhq/(?!ui$)([^/]+)$':
|
||||
'<rootDir>/node_modules/@signozhq/$1/dist/$1.js',
|
||||
'^@signozhq/([^/]+)$': '<rootDir>/node_modules/@signozhq/$1/dist/$1.js',
|
||||
},
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
"@signozhq/table": "0.3.7",
|
||||
"@signozhq/toggle-group": "0.0.1",
|
||||
"@signozhq/tooltip": "0.0.2",
|
||||
"@signozhq/ui": "0.0.5",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
@@ -776,45 +776,6 @@ export interface GatewaytypesUpdatableIngestionKeyLimitDTO {
|
||||
tags?: string[] | null;
|
||||
}
|
||||
|
||||
export interface GlobaltypesAPIKeyConfigDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface GlobaltypesConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
external_url?: string;
|
||||
identN?: GlobaltypesIdentNConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
ingestion_url?: string;
|
||||
}
|
||||
|
||||
export interface GlobaltypesIdentNConfigDTO {
|
||||
apikey?: GlobaltypesAPIKeyConfigDTO;
|
||||
impersonation?: GlobaltypesImpersonationConfigDTO;
|
||||
tokenizer?: GlobaltypesTokenizerConfigDTO;
|
||||
}
|
||||
|
||||
export interface GlobaltypesImpersonationConfigDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface GlobaltypesTokenizerConfigDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesListMetricDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -2441,6 +2402,17 @@ export interface TypesGettableAPIKeyDTO {
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export interface TypesGettableGlobalConfigDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
external_url?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
ingestion_url?: string;
|
||||
}
|
||||
|
||||
export interface TypesIdentifiableDTO {
|
||||
/**
|
||||
* @type string
|
||||
@@ -2478,11 +2450,6 @@ export interface TypesInviteDTO {
|
||||
* @type string
|
||||
*/
|
||||
role?: string;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
roles?: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -2602,11 +2569,6 @@ export interface TypesPostableInviteDTO {
|
||||
* @type string
|
||||
*/
|
||||
role?: string;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
roles?: string[] | null;
|
||||
}
|
||||
|
||||
export interface TypesPostableResetPasswordDTO {
|
||||
@@ -2715,11 +2677,6 @@ export interface TypesUserDTO {
|
||||
* @type string
|
||||
*/
|
||||
role?: string;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
roles?: string[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
@@ -3069,7 +3026,7 @@ export type GetResetPasswordToken200 = {
|
||||
};
|
||||
|
||||
export type GetGlobalConfig200 = {
|
||||
data: GlobaltypesConfigDTO;
|
||||
data: TypesGettableGlobalConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -81,8 +81,7 @@ export const interceptorRejected = async (
|
||||
response.config.url !== '/sessions/email_password' &&
|
||||
!(
|
||||
response.config.url === '/sessions' && response.config.method === 'delete'
|
||||
) &&
|
||||
response.config.url !== '/authz/check'
|
||||
)
|
||||
) {
|
||||
try {
|
||||
const accessToken = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN);
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import axios, { AxiosHeaders, AxiosResponse } from 'axios';
|
||||
|
||||
import { interceptorRejected } from './index';
|
||||
|
||||
jest.mock('api/browser/localstorage/get', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => 'mock-token'),
|
||||
}));
|
||||
|
||||
jest.mock('api/v2/sessions/rotate/post', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
data: { accessToken: 'new-token', refreshToken: 'new-refresh' },
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('AppRoutes/utils', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('axios', () => {
|
||||
const actualAxios = jest.requireActual('axios');
|
||||
const mockAxios = jest.fn().mockResolvedValue({ data: 'success' });
|
||||
|
||||
return {
|
||||
...actualAxios,
|
||||
default: Object.assign(mockAxios, {
|
||||
...actualAxios.default,
|
||||
isAxiosError: jest.fn().mockReturnValue(true),
|
||||
create: actualAxios.create,
|
||||
}),
|
||||
__esModule: true,
|
||||
};
|
||||
});
|
||||
|
||||
describe('interceptorRejected', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
((axios as unknown) as jest.Mock).mockResolvedValue({ data: 'success' });
|
||||
((axios.isAxiosError as unknown) as jest.Mock).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('should preserve array payload structure when retrying a 401 request', async () => {
|
||||
const arrayPayload = [
|
||||
{ relation: 'assignee', object: { resource: { name: 'role' } } },
|
||||
{ relation: 'assignee', object: { resource: { name: 'editor' } } },
|
||||
];
|
||||
|
||||
const error = ({
|
||||
response: {
|
||||
status: 401,
|
||||
config: {
|
||||
url: '/some-endpoint',
|
||||
method: 'POST',
|
||||
baseURL: 'http://localhost/',
|
||||
headers: new AxiosHeaders(),
|
||||
data: JSON.stringify(arrayPayload),
|
||||
},
|
||||
},
|
||||
config: {
|
||||
url: '/some-endpoint',
|
||||
method: 'POST',
|
||||
baseURL: 'http://localhost/',
|
||||
headers: new AxiosHeaders(),
|
||||
data: JSON.stringify(arrayPayload),
|
||||
},
|
||||
} as unknown) as AxiosResponse;
|
||||
|
||||
try {
|
||||
await interceptorRejected(error);
|
||||
} catch {
|
||||
// Expected to reject after retry
|
||||
}
|
||||
|
||||
const mockAxiosFn = (axios as unknown) as jest.Mock;
|
||||
expect(mockAxiosFn.mock.calls.length).toBe(1);
|
||||
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
|
||||
expect(Array.isArray(JSON.parse(retryCallConfig.data))).toBe(true);
|
||||
expect(JSON.parse(retryCallConfig.data)).toEqual(arrayPayload);
|
||||
});
|
||||
|
||||
it('should preserve object payload structure when retrying a 401 request', async () => {
|
||||
const objectPayload = { key: 'value', nested: { data: 123 } };
|
||||
|
||||
const error = ({
|
||||
response: {
|
||||
status: 401,
|
||||
config: {
|
||||
url: '/some-endpoint',
|
||||
method: 'POST',
|
||||
baseURL: 'http://localhost/',
|
||||
headers: new AxiosHeaders(),
|
||||
data: JSON.stringify(objectPayload),
|
||||
},
|
||||
},
|
||||
config: {
|
||||
url: '/some-endpoint',
|
||||
method: 'POST',
|
||||
baseURL: 'http://localhost/',
|
||||
headers: new AxiosHeaders(),
|
||||
data: JSON.stringify(objectPayload),
|
||||
},
|
||||
} as unknown) as AxiosResponse;
|
||||
|
||||
try {
|
||||
await interceptorRejected(error);
|
||||
} catch {
|
||||
// Expected to reject after retry
|
||||
}
|
||||
|
||||
const mockAxiosFn = (axios as unknown) as jest.Mock;
|
||||
expect(mockAxiosFn.mock.calls.length).toBe(1);
|
||||
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
|
||||
expect(JSON.parse(retryCallConfig.data)).toEqual(objectPayload);
|
||||
});
|
||||
|
||||
it('should handle undefined data gracefully when retrying', async () => {
|
||||
const error = ({
|
||||
response: {
|
||||
status: 401,
|
||||
config: {
|
||||
url: '/some-endpoint',
|
||||
method: 'GET',
|
||||
baseURL: 'http://localhost/',
|
||||
headers: new AxiosHeaders(),
|
||||
data: undefined,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
url: '/some-endpoint',
|
||||
method: 'GET',
|
||||
baseURL: 'http://localhost/',
|
||||
headers: new AxiosHeaders(),
|
||||
data: undefined,
|
||||
},
|
||||
} as unknown) as AxiosResponse;
|
||||
|
||||
try {
|
||||
await interceptorRejected(error);
|
||||
} catch {
|
||||
// Expected to reject after retry
|
||||
}
|
||||
|
||||
const mockAxiosFn = (axios as unknown) as jest.Mock;
|
||||
expect(mockAxiosFn.mock.calls.length).toBe(1);
|
||||
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
|
||||
expect(retryCallConfig.data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,8 @@
|
||||
function UnAuthorized({
|
||||
width = 137,
|
||||
height = 137,
|
||||
}: {
|
||||
height?: number;
|
||||
width?: number;
|
||||
}): JSX.Element {
|
||||
function UnAuthorized(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
width="137"
|
||||
height="137"
|
||||
viewBox="0 0 137 137"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
1
frontend/src/auto-import-registry.d.ts
vendored
1
frontend/src/auto-import-registry.d.ts
vendored
@@ -30,4 +30,3 @@ import '@signozhq/switch';
|
||||
import '@signozhq/table';
|
||||
import '@signozhq/toggle-group';
|
||||
import '@signozhq/tooltip';
|
||||
import '@signozhq/ui';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createShortcutActions } from '../../constants/shortcutActions';
|
||||
import { useCmdK } from '../../providers/cmdKProvider';
|
||||
import { ROLES } from '../../types/roles';
|
||||
import { ShiftOverlay } from './ShiftOverlay';
|
||||
import { useShiftHoldOverlay } from './useShiftHoldOverlay';
|
||||
|
||||
type UserRole = 'ADMIN' | 'EDITOR' | 'AUTHOR' | 'VIEWER';
|
||||
export function ShiftHoldOverlayController({
|
||||
userRole,
|
||||
}: {
|
||||
userRole: ROLES;
|
||||
userRole: UserRole;
|
||||
}): JSX.Element | null {
|
||||
const { open: isCmdKOpen } = useCmdK();
|
||||
const noop = (): void => undefined;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useMemo } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import { formatShortcut } from './formatShortcut';
|
||||
|
||||
import './shiftOverlay.scss';
|
||||
|
||||
export type UserRole = 'ADMIN' | 'EDITOR' | 'AUTHOR' | 'VIEWER';
|
||||
export type CmdAction = {
|
||||
id: string;
|
||||
name: string;
|
||||
shortcut?: string[];
|
||||
keywords?: string;
|
||||
section?: string;
|
||||
roles?: ROLES[];
|
||||
roles?: UserRole[];
|
||||
perform: () => void;
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ function Shortcut({ label, keyHint }: ShortcutProps): JSX.Element {
|
||||
interface ShiftOverlayProps {
|
||||
visible: boolean;
|
||||
actions: CmdAction[];
|
||||
userRole: ROLES;
|
||||
userRole: UserRole;
|
||||
}
|
||||
|
||||
export function ShiftOverlay({
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useThemeMode } from 'hooks/useDarkMode';
|
||||
import history from 'lib/history';
|
||||
import { ROLES as UserRole } from 'types/roles';
|
||||
|
||||
import { createShortcutActions } from '../../constants/shortcutActions';
|
||||
import { useCmdK } from '../../providers/cmdKProvider';
|
||||
@@ -29,6 +28,7 @@ type CmdAction = {
|
||||
perform: () => void;
|
||||
};
|
||||
|
||||
type UserRole = 'ADMIN' | 'EDITOR' | 'AUTHOR' | 'VIEWER';
|
||||
export function CmdKPalette({
|
||||
userRole,
|
||||
}: {
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
TowerControl,
|
||||
Workflow,
|
||||
} from 'lucide-react';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
export type UserRole = 'ADMIN' | 'EDITOR' | 'AUTHOR' | 'VIEWER';
|
||||
|
||||
export type CmdAction = {
|
||||
id: string;
|
||||
@@ -27,7 +28,7 @@ export type CmdAction = {
|
||||
keywords?: string;
|
||||
section?: string;
|
||||
icon?: React.ReactNode;
|
||||
roles?: ROLES[];
|
||||
roles?: UserRole[];
|
||||
perform: () => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,14 +3,16 @@ import { UseQueryResult } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Card, Skeleton, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { useGetGraphCustomSeries } from 'components/CeleryTask/useGetGraphCustomSeries';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
getCustomFiltersForBarChart,
|
||||
getFormattedEndPointStatusCodeChartData,
|
||||
getStatusCodeBarChartWidgetData,
|
||||
statusCodeWidgetInfo,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
@@ -18,16 +20,15 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { Options } from 'uplot';
|
||||
|
||||
import ErrorState from './ErrorState';
|
||||
import { prepareStatusCodeBarChartsConfig } from './utils';
|
||||
|
||||
function StatusCodeBarCharts({
|
||||
endPointStatusCodeBarChartsDataQuery,
|
||||
@@ -66,6 +67,13 @@ function StatusCodeBarCharts({
|
||||
} = endPointStatusCodeLatencyBarChartsDataQuery;
|
||||
|
||||
const { startTime: minTime, endTime: maxTime } = timeRange;
|
||||
const legendScrollPositionRef = useRef<{
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}>({
|
||||
scrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
});
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const dimensions = useResizeObserver(graphRef);
|
||||
@@ -111,7 +119,6 @@ function StatusCodeBarCharts({
|
||||
|
||||
const navigateToExplorer = useNavigateToExplorer();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
const navigateToExplorerPages = useNavigateToExplorerPages();
|
||||
const { notifications } = useNotifications();
|
||||
@@ -127,6 +134,12 @@ function StatusCodeBarCharts({
|
||||
[],
|
||||
);
|
||||
|
||||
const { getCustomSeries } = useGetGraphCustomSeries({
|
||||
isDarkMode,
|
||||
drawStyle: 'bars',
|
||||
colorMapping,
|
||||
});
|
||||
|
||||
const widget = useMemo<Widgets>(
|
||||
() =>
|
||||
getStatusCodeBarChartWidgetData(domainName, {
|
||||
@@ -180,36 +193,49 @@ function StatusCodeBarCharts({
|
||||
],
|
||||
);
|
||||
|
||||
const config = useMemo(() => {
|
||||
const apiResponse =
|
||||
currentWidgetInfoIndex === 0
|
||||
? formattedEndPointStatusCodeBarChartsDataPayload
|
||||
: formattedEndPointStatusCodeLatencyBarChartsDataPayload;
|
||||
return prepareStatusCodeBarChartsConfig({
|
||||
timezone,
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
apiResponse:
|
||||
currentWidgetInfoIndex === 0
|
||||
? formattedEndPointStatusCodeBarChartsDataPayload
|
||||
: formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||
isDarkMode,
|
||||
dimensions,
|
||||
yAxisUnit: statusCodeWidgetInfo[currentWidgetInfoIndex].yAxisUnit,
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
minTimeScale: minTime,
|
||||
maxTimeScale: maxTime,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
onClickHandler: graphClickHandler,
|
||||
customSeries: getCustomSeries,
|
||||
onDragSelect,
|
||||
colorMapping,
|
||||
query: currentQuery,
|
||||
legendScrollPosition: legendScrollPositionRef.current,
|
||||
setLegendScrollPosition: (position: {
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}) => {
|
||||
legendScrollPositionRef.current = position;
|
||||
},
|
||||
}),
|
||||
[
|
||||
minTime,
|
||||
maxTime,
|
||||
currentWidgetInfoIndex,
|
||||
dimensions,
|
||||
formattedEndPointStatusCodeBarChartsDataPayload,
|
||||
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||
isDarkMode,
|
||||
query: currentQuery,
|
||||
graphClickHandler,
|
||||
getCustomSeries,
|
||||
onDragSelect,
|
||||
onClick: graphClickHandler,
|
||||
apiResponse,
|
||||
minTimeScale: minTime,
|
||||
maxTimeScale: maxTime,
|
||||
yAxisUnit: statusCodeWidgetInfo[currentWidgetInfoIndex].yAxisUnit,
|
||||
colorMapping,
|
||||
});
|
||||
}, [
|
||||
currentQuery,
|
||||
isDarkMode,
|
||||
minTime,
|
||||
maxTime,
|
||||
graphClickHandler,
|
||||
onDragSelect,
|
||||
formattedEndPointStatusCodeBarChartsDataPayload,
|
||||
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||
timezone,
|
||||
currentWidgetInfoIndex,
|
||||
colorMapping,
|
||||
]);
|
||||
currentQuery,
|
||||
],
|
||||
);
|
||||
|
||||
const renderCardContent = useCallback(
|
||||
(query: UseQueryResult<SuccessResponse<any>, unknown>): JSX.Element => {
|
||||
@@ -227,20 +253,11 @@ function StatusCodeBarCharts({
|
||||
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||
})}
|
||||
>
|
||||
<BarChart
|
||||
config={config}
|
||||
data={chartData}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
timezone={timezone}
|
||||
legendConfig={{
|
||||
position: LegendPosition.BOTTOM,
|
||||
}}
|
||||
/>
|
||||
<Uplot options={options as Options} data={chartData} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[config, chartData, dimensions, timezone],
|
||||
[options, chartData],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { ExecStats } from 'api/v5/v5';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { DrawStyle } from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { get } from 'lodash-es';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const prepareStatusCodeBarChartsConfig = ({
|
||||
timezone,
|
||||
isDarkMode,
|
||||
query,
|
||||
onDragSelect,
|
||||
onClick,
|
||||
apiResponse,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
yAxisUnit,
|
||||
colorMapping,
|
||||
}: {
|
||||
timezone: Timezone;
|
||||
isDarkMode: boolean;
|
||||
query: Query;
|
||||
onDragSelect: (startTime: number, endTime: number) => void;
|
||||
onClick?: OnClickPluginOpts['onClick'];
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
apiResponse: MetricRangePayloadProps;
|
||||
yAxisUnit?: string;
|
||||
colorMapping?: Record<string, string>;
|
||||
}): UPlotConfigBuilder => {
|
||||
const stepIntervals: ExecStats['stepIntervals'] = get(
|
||||
apiResponse,
|
||||
'data.newResult.meta.stepIntervals',
|
||||
{},
|
||||
);
|
||||
const minStepInterval = Math.min(...Object.values(stepIntervals));
|
||||
|
||||
const config = buildBaseConfig({
|
||||
id: v4(),
|
||||
yAxisUnit: yAxisUnit,
|
||||
apiResponse,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
timezone,
|
||||
onClick,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
stepInterval: minStepInterval,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
});
|
||||
|
||||
const seriesList: QueryData[] = apiResponse?.data?.result || [];
|
||||
seriesList.forEach((series) => {
|
||||
const baseLabelName = getLabelName(
|
||||
series.metric,
|
||||
series.queryName || '', // query
|
||||
series.legend || '',
|
||||
);
|
||||
|
||||
const label = query ? getLegend(series, query, baseLabelName) : baseLabelName;
|
||||
|
||||
const currentStepInterval = get(stepIntervals, series.queryName, undefined);
|
||||
|
||||
config.addSeries({
|
||||
scaleKey: 'y',
|
||||
drawStyle: DrawStyle.Bar,
|
||||
label: label,
|
||||
colorMapping: colorMapping ?? {},
|
||||
isDarkMode,
|
||||
stepInterval: currentStepInterval,
|
||||
});
|
||||
});
|
||||
|
||||
return config;
|
||||
};
|
||||
@@ -21,15 +21,10 @@ interface MockQueryResult {
|
||||
}
|
||||
|
||||
// Mocks
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/BarChart/BarChart',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div data-testid="bar-chart-mock" />),
|
||||
}),
|
||||
);
|
||||
jest.mock('components/Uplot', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => <div data-testid="uplot-mock" />),
|
||||
}));
|
||||
|
||||
jest.mock('components/CeleryTask/useGetGraphCustomSeries', () => ({
|
||||
useGetGraphCustomSeries: (): { getCustomSeries: jest.Mock } => ({
|
||||
@@ -75,24 +70,6 @@ jest.mock('hooks/useNotifications', () => ({
|
||||
useNotifications: (): { notifications: [] } => ({ notifications: [] }),
|
||||
}));
|
||||
|
||||
jest.mock('providers/Timezone', () => ({
|
||||
useTimezone: (): {
|
||||
timezone: {
|
||||
name: string;
|
||||
value: string;
|
||||
offset: string;
|
||||
searchIndex: string;
|
||||
};
|
||||
} => ({
|
||||
timezone: {
|
||||
name: 'UTC',
|
||||
value: 'UTC',
|
||||
offset: '+00:00',
|
||||
searchIndex: 'UTC',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('lib/uPlotLib/getUplotChartOptions', () => ({
|
||||
getUPlotChartOptions: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
@@ -342,7 +319,7 @@ describe('StatusCodeBarCharts', () => {
|
||||
mockData.payload,
|
||||
'sum',
|
||||
);
|
||||
expect(screen.getByTestId('bar-chart-mock')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('uplot-mock')).toBeInTheDocument();
|
||||
expect(screen.getByText('Number of calls')).toBeInTheDocument();
|
||||
expect(screen.getByText('Latency')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
LegendPosition,
|
||||
TooltipRenderArgs,
|
||||
} from 'lib/uPlotV2/components/types';
|
||||
import UPlotChart from 'lib/uPlotV2/components/UPlotChart/UPlotChart';
|
||||
import UPlotChart from 'lib/uPlotV2/components/UPlotChart';
|
||||
import { PlotContextProvider } from 'lib/uPlotV2/context/PlotContext';
|
||||
import TooltipPlugin from 'lib/uPlotV2/plugins/TooltipPlugin/TooltipPlugin';
|
||||
import noop from 'lodash-es/noop';
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -337,6 +337,31 @@
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Button } from '@signozhq/ui';
|
||||
import { Button } from '@signozhq/button';
|
||||
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}
|
||||
testId="initiate_login"
|
||||
data-testid="initiate_login"
|
||||
className="login-submit-btn"
|
||||
suffix={<ArrowRight />}
|
||||
suffixIcon={<ArrowRight size={12} />}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
@@ -406,10 +406,10 @@ function Login(): JSX.Element {
|
||||
variant="solid"
|
||||
type="submit"
|
||||
color="primary"
|
||||
testId="callback_authn_submit"
|
||||
data-testid="callback_authn_submit"
|
||||
data-attr="signup"
|
||||
className="login-submit-btn"
|
||||
suffix={<ArrowRight />}
|
||||
suffixIcon={<ArrowRight size={12} />}
|
||||
>
|
||||
Sign in with SSO
|
||||
</Button>
|
||||
@@ -420,11 +420,11 @@ function Login(): JSX.Element {
|
||||
disabled={!isSubmitButtonEnabled}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
testId="password_authn_submit"
|
||||
data-testid="password_authn_submit"
|
||||
type="submit"
|
||||
data-attr="signup"
|
||||
className="login-submit-btn"
|
||||
suffix={<ArrowRight />}
|
||||
suffixIcon={<ArrowRight size={12} />}
|
||||
>
|
||||
Sign in with Password
|
||||
</Button>
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
font-family: 'Space Mono';
|
||||
padding-bottom: 48px;
|
||||
|
||||
.panel-type-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-family: 'Space Mono';
|
||||
color: var(--bg-vanilla-400);
|
||||
@@ -30,6 +26,10 @@
|
||||
letter-spacing: 0.48px;
|
||||
}
|
||||
|
||||
.panel-type-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 14px 14px 14px 12px;
|
||||
@@ -216,8 +216,7 @@
|
||||
.lightMode {
|
||||
.right-container {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
.section-heading,
|
||||
.section-heading-small {
|
||||
.section-heading {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
.header {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,8 +178,6 @@ describe('RightContainer - Alerts Section', () => {
|
||||
setLineStyle: jest.fn(),
|
||||
showPoints: false,
|
||||
setShowPoints: jest.fn(),
|
||||
spanGaps: false,
|
||||
setSpanGaps: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,38 +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}
|
||||
size="lg"
|
||||
onValueChange={(newValue): void => {
|
||||
if (newValue) {
|
||||
onChange(newValue as DisconnectedValuesMode);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleGroupItem value={DisconnectedValuesMode.Never} aria-label="Never">
|
||||
<Typography.Text className="section-heading-small">Never</Typography.Text>
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value={DisconnectedValuesMode.Threshold}
|
||||
aria-label="Threshold"
|
||||
>
|
||||
<Typography.Text className="section-heading-small">
|
||||
Threshold
|
||||
</Typography.Text>
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { rangeUtil } from '@grafana/data';
|
||||
import { Callout, 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 updateValue = (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') {
|
||||
updateValue(e.currentTarget.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement>): void => {
|
||||
updateValue(e.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setInputValue(e.currentTarget.value);
|
||||
if (error) {
|
||||
setError(null);
|
||||
}
|
||||
};
|
||||
|
||||
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">></span>}
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
autoFocus={true}
|
||||
aria-invalid={!!error}
|
||||
aria-describedby={error ? 'threshold-error' : undefined}
|
||||
/>
|
||||
{error && (
|
||||
<Callout type="error" size="small" showIcon>
|
||||
{error}
|
||||
</Callout>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ interface FillModeSelectorProps {
|
||||
onChange: (value: FillMode) => void;
|
||||
}
|
||||
|
||||
export default function FillModeSelector({
|
||||
export function FillModeSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: FillModeSelectorProps): JSX.Element {
|
||||
|
||||
@@ -9,7 +9,7 @@ interface LineInterpolationSelectorProps {
|
||||
onChange: (value: LineInterpolation) => void;
|
||||
}
|
||||
|
||||
export default function LineInterpolationSelector({
|
||||
export function LineInterpolationSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: LineInterpolationSelectorProps): JSX.Element {
|
||||
|
||||
@@ -9,7 +9,7 @@ interface LineStyleSelectorProps {
|
||||
onChange: (value: LineStyle) => void;
|
||||
}
|
||||
|
||||
export default function LineStyleSelector({
|
||||
export function LineStyleSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: LineStyleSelectorProps): JSX.Element {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import i18n from 'ReactI18';
|
||||
import {
|
||||
fireEvent,
|
||||
getByText as getByTextUtil,
|
||||
render,
|
||||
userEvent,
|
||||
@@ -341,8 +342,9 @@ describe('Stacking bar in new panel', () => {
|
||||
const STACKING_STATE_ATTR = 'data-stacking-state';
|
||||
|
||||
describe('when switching to BAR panel type', () => {
|
||||
jest.setTimeout(10000);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Mock useSearchParams to return the expected values
|
||||
@@ -352,15 +354,7 @@ describe('when switching to BAR panel type', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should preserve saved stacking value of true', async () => {
|
||||
const user = userEvent.setup({
|
||||
advanceTimers: jest.advanceTimersByTime.bind(jest),
|
||||
});
|
||||
|
||||
const { getByTestId, getByText, container } = render(
|
||||
<DashboardProvider dashboardId="">
|
||||
<NewWidget
|
||||
@@ -376,7 +370,7 @@ describe('when switching to BAR panel type', () => {
|
||||
'true',
|
||||
);
|
||||
|
||||
await user.click(getByText('Bar')); // Panel Type Selected
|
||||
await userEvent.click(getByText('Bar')); // Panel Type Selected
|
||||
|
||||
// find dropdown with - .ant-select-dropdown
|
||||
const panelDropdown = document.querySelector(
|
||||
@@ -386,7 +380,7 @@ describe('when switching to BAR panel type', () => {
|
||||
|
||||
// Select TimeSeries from dropdown
|
||||
const option = within(panelDropdown).getByText('Time Series');
|
||||
await user.click(option);
|
||||
fireEvent.click(option);
|
||||
|
||||
expect(getByTestId('panel-change-select')).toHaveAttribute(
|
||||
STACKING_STATE_ATTR,
|
||||
@@ -401,7 +395,7 @@ describe('when switching to BAR panel type', () => {
|
||||
expect(panelTypeDropdown2).toBeInTheDocument();
|
||||
|
||||
expect(getByTextUtil(panelTypeDropdown2, 'Time Series')).toBeInTheDocument();
|
||||
await user.click(getByTextUtil(panelTypeDropdown2, 'Time Series'));
|
||||
fireEvent.click(getByTextUtil(panelTypeDropdown2, 'Time Series'));
|
||||
|
||||
// find dropdown with - .ant-select-dropdown
|
||||
const panelDropdown2 = document.querySelector(
|
||||
@@ -409,7 +403,7 @@ describe('when switching to BAR panel type', () => {
|
||||
) as HTMLElement;
|
||||
// // Select BAR from dropdown
|
||||
const BarOption = within(panelDropdown2).getByText('Bar');
|
||||
await user.click(BarOption);
|
||||
fireEvent.click(BarOption);
|
||||
|
||||
// Stack series should be true
|
||||
checkStackSeriesState(container, true);
|
||||
|
||||
@@ -220,9 +220,6 @@ function NewWidget({
|
||||
const [showPoints, setShowPoints] = useState<boolean>(
|
||||
selectedWidget?.showPoints ?? false,
|
||||
);
|
||||
const [spanGaps, setSpanGaps] = useState<boolean | number>(
|
||||
selectedWidget?.spanGaps ?? true,
|
||||
);
|
||||
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}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const SINGLE_FLIGHT_WAIT_TIME_MS = 50;
|
||||
export const AUTHZ_CACHE_TIME = 20_000;
|
||||
@@ -1,18 +0,0 @@
|
||||
import { buildPermission } from './utils';
|
||||
|
||||
export const IsAdminPermission = buildPermission(
|
||||
'assignee',
|
||||
'role:signoz-admin',
|
||||
);
|
||||
export const IsEditorPermission = buildPermission(
|
||||
'assignee',
|
||||
'role:signoz-editor',
|
||||
);
|
||||
export const IsViewerPermission = buildPermission(
|
||||
'assignee',
|
||||
'role:signoz-viewer',
|
||||
);
|
||||
export const IsAnonymousPermission = buildPermission(
|
||||
'assignee',
|
||||
'role:signoz-anonymous',
|
||||
);
|
||||
@@ -14,7 +14,7 @@ type ResourceTypeMap = {
|
||||
|
||||
type RelationName = keyof RelationsByType;
|
||||
|
||||
export type ResourcesForRelation<R extends RelationName> = Extract<
|
||||
type ResourcesForRelation<R extends RelationName> = Extract<
|
||||
Resource,
|
||||
{ type: RelationsByType[R][number] }
|
||||
>['name'];
|
||||
@@ -50,26 +50,8 @@ export type AuthZCheckResponse = Record<
|
||||
}
|
||||
>;
|
||||
|
||||
export type UseAuthZOptions = {
|
||||
/**
|
||||
* If false, the query/permissions will not be fetched.
|
||||
* Useful when you want to disable the query/permissions for a specific use case, like logout.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export type UseAuthZResult = {
|
||||
/**
|
||||
* If query is cached, and refetch happens in background, this is false.
|
||||
*/
|
||||
isLoading: boolean;
|
||||
/**
|
||||
* If query is fetching, even if happens in background, this is true.
|
||||
*/
|
||||
isFetching: boolean;
|
||||
error: Error | null;
|
||||
permissions: AuthZCheckResponse | null;
|
||||
refetchPermissions: () => void;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { authzCheck } from 'api/generated/services/authz';
|
||||
import type {
|
||||
@@ -6,13 +6,7 @@ import type {
|
||||
AuthtypesTransactionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { AUTHZ_CACHE_TIME, SINGLE_FLIGHT_WAIT_TIME_MS } from './constants';
|
||||
import {
|
||||
AuthZCheckResponse,
|
||||
BrandedPermission,
|
||||
UseAuthZOptions,
|
||||
UseAuthZResult,
|
||||
} from './types';
|
||||
import { AuthZCheckResponse, BrandedPermission, UseAuthZResult } from './types';
|
||||
import {
|
||||
gettableTransactionToPermission,
|
||||
permissionToTransactionDto,
|
||||
@@ -20,6 +14,8 @@ import {
|
||||
|
||||
let ctx: Promise<AuthZCheckResponse> | null;
|
||||
let pendingPermissions: BrandedPermission[] = [];
|
||||
const SINGLE_FLIGHT_WAIT_TIME_MS = 50;
|
||||
const AUTHZ_CACHE_TIME = 20_000;
|
||||
|
||||
function dispatchPermission(
|
||||
permission: BrandedPermission,
|
||||
@@ -74,12 +70,7 @@ async function fetchManyPermissions(
|
||||
}, {} as AuthZCheckResponse);
|
||||
}
|
||||
|
||||
export function useAuthZ(
|
||||
permissions: BrandedPermission[],
|
||||
options?: UseAuthZOptions,
|
||||
): UseAuthZResult {
|
||||
const { enabled } = options ?? { enabled: true };
|
||||
|
||||
export function useAuthZ(permissions: BrandedPermission[]): UseAuthZResult {
|
||||
const queryResults = useQueries(
|
||||
permissions.map((permission) => {
|
||||
return {
|
||||
@@ -89,7 +80,6 @@ export function useAuthZ(
|
||||
refetchIntervalInBackground: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: true,
|
||||
enabled,
|
||||
queryFn: async (): Promise<AuthZCheckResponse> => {
|
||||
const response = await dispatchPermission(permission);
|
||||
|
||||
@@ -106,10 +96,6 @@ export function useAuthZ(
|
||||
const isLoading = useMemo(() => queryResults.some((q) => q.isLoading), [
|
||||
queryResults,
|
||||
]);
|
||||
const isFetching = useMemo(() => queryResults.some((q) => q.isFetching), [
|
||||
queryResults,
|
||||
]);
|
||||
|
||||
const error = useMemo(
|
||||
() =>
|
||||
!isLoading
|
||||
@@ -135,17 +121,9 @@ export function useAuthZ(
|
||||
}, {} as AuthZCheckResponse);
|
||||
}, [isLoading, error, queryResults]);
|
||||
|
||||
const refetchPermissions = useCallback(() => {
|
||||
for (const query of queryResults) {
|
||||
query.refetch();
|
||||
}
|
||||
}, [queryResults]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
isFetching,
|
||||
error,
|
||||
permissions: data ?? null,
|
||||
refetchPermissions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import permissionsType from './permissions.type';
|
||||
import {
|
||||
AuthZObject,
|
||||
AuthZRelation,
|
||||
AuthZResource,
|
||||
BrandedPermission,
|
||||
ResourceName,
|
||||
ResourcesForRelation,
|
||||
ResourceType,
|
||||
} from './types';
|
||||
|
||||
@@ -19,10 +19,11 @@ export function buildPermission<R extends AuthZRelation>(
|
||||
return `${relation}${PermissionSeparator}${object}` as BrandedPermission;
|
||||
}
|
||||
|
||||
export function buildObjectString<
|
||||
R extends 'delete' | 'read' | 'update' | 'assignee'
|
||||
>(resource: ResourcesForRelation<R>, objectId: string): AuthZObject<R> {
|
||||
return `${resource}${ObjectSeparator}${objectId}` as AuthZObject<R>;
|
||||
export function buildObjectString(
|
||||
resource: AuthZResource,
|
||||
objectId: string,
|
||||
): `${AuthZResource}${typeof ObjectSeparator}${string}` {
|
||||
return `${resource}${ObjectSeparator}${objectId}` as const;
|
||||
}
|
||||
|
||||
export function parsePermission(
|
||||
|
||||
@@ -6,9 +6,8 @@ import { LineChart } from 'lucide-react';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import uPlot, { AlignedData, Options } from 'uplot';
|
||||
|
||||
import { usePlotContext } from '../../context/PlotContext';
|
||||
import { UPlotChartProps } from '../types';
|
||||
import { prepareAlignedData } from './utils';
|
||||
import { usePlotContext } from '../context/PlotContext';
|
||||
import { UPlotChartProps } from './types';
|
||||
|
||||
/**
|
||||
* Check if dimensions have changed
|
||||
@@ -84,11 +83,8 @@ export default function UPlotChart({
|
||||
...configOptions,
|
||||
} as Options;
|
||||
|
||||
// prepare final AlignedData
|
||||
const preparedData = prepareAlignedData({ data, config });
|
||||
|
||||
// Create new plot instance
|
||||
const plot = new uPlot(plotConfig, preparedData, containerRef.current);
|
||||
const plot = new uPlot(plotConfig, data as AlignedData, containerRef.current);
|
||||
|
||||
if (plotRef) {
|
||||
plotRef(plot);
|
||||
@@ -166,8 +162,7 @@ export default function UPlotChart({
|
||||
}
|
||||
// Update data if only data changed
|
||||
else if (!sameData(prevProps, currentProps) && plotInstanceRef.current) {
|
||||
const preparedData = prepareAlignedData({ data, config });
|
||||
plotInstanceRef.current.setData(preparedData as AlignedData);
|
||||
plotInstanceRef.current.setData(data as AlignedData);
|
||||
}
|
||||
|
||||
prevPropsRef.current = currentProps;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { applySpanGapsToAlignedData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import { AlignedData } from 'uplot';
|
||||
|
||||
export function prepareAlignedData({
|
||||
data,
|
||||
config,
|
||||
}: {
|
||||
data: AlignedData;
|
||||
config: UPlotConfigBuilder;
|
||||
}): AlignedData {
|
||||
const seriesSpanGaps = config.getSeriesSpanGapsOptions();
|
||||
return seriesSpanGaps.length > 0
|
||||
? applySpanGapsToAlignedData(data as AlignedData, seriesSpanGaps)
|
||||
: (data as AlignedData);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import type { AlignedData } from 'uplot';
|
||||
|
||||
import { PlotContextProvider } from '../../context/PlotContext';
|
||||
import UPlotChart from '../UPlotChart/UPlotChart';
|
||||
import UPlotChart from '../UPlotChart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -212,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 true.
|
||||
spanGaps: typeof spanGaps === 'number' ? false : spanGaps ?? true,
|
||||
spanGaps: typeof spanGaps === 'boolean' ? spanGaps : false,
|
||||
value: (): string => '',
|
||||
pxAlign: true,
|
||||
show,
|
||||
|
||||
@@ -40,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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,10 +24,10 @@ export function isInvalidPlotValue(value: unknown): boolean {
|
||||
}
|
||||
|
||||
// Try to parse the string as a number
|
||||
const parsedNumber = parseFloat(value);
|
||||
const numValue = parseFloat(value);
|
||||
|
||||
// If parsing failed or resulted in a non-finite number, it's invalid
|
||||
if (Number.isNaN(parsedNumber) || !Number.isFinite(parsedNumber)) {
|
||||
if (Number.isNaN(numValue) || !Number.isFinite(numValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -51,178 +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 timestamps = xValues as number[];
|
||||
const totalPoints = timestamps.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 < totalPoints - 1; i += 1) {
|
||||
if (gapExceedsThreshold(timestamps[i + 1] - timestamps[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.
|
||||
// `writeIndex` is the write cursor into the output arrays.
|
||||
const outputLen = totalPoints + insertionCount;
|
||||
const newX = new Array<number>(outputLen);
|
||||
const newSeries: SeriesArray[] = seriesValues.map(
|
||||
() => new Array<number | null | undefined>(outputLen),
|
||||
);
|
||||
|
||||
let writeIndex = 0;
|
||||
for (let i = 0; i < totalPoints; i += 1) {
|
||||
// Copy the real data point at position i
|
||||
newX[writeIndex] = timestamps[i];
|
||||
for (
|
||||
let seriesIndex = 0;
|
||||
seriesIndex < seriesValues.length;
|
||||
seriesIndex += 1
|
||||
) {
|
||||
newSeries[seriesIndex][writeIndex] = (seriesValues[
|
||||
seriesIndex
|
||||
] as SeriesArray)[i];
|
||||
}
|
||||
writeIndex += 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 timestamps[i] and timestamps[i+1] (minimum 1 unit past timestamps[i] to stay unique).
|
||||
if (
|
||||
i < totalPoints - 1 &&
|
||||
gapExceedsThreshold(timestamps[i + 1] - timestamps[i], seriesOptions)
|
||||
) {
|
||||
newX[writeIndex] =
|
||||
timestamps[i] +
|
||||
Math.max(1, Math.floor((timestamps[i + 1] - timestamps[i]) / 2));
|
||||
for (
|
||||
let seriesIndex = 0;
|
||||
seriesIndex < seriesValues.length;
|
||||
seriesIndex += 1
|
||||
) {
|
||||
newSeries[seriesIndex][writeIndex] = null; // null tells uPlot to break the line here
|
||||
}
|
||||
writeIndex += 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((yValues, seriesIndex) => {
|
||||
const { spanGaps } = seriesOptions[seriesIndex] ?? {};
|
||||
if (spanGaps !== true) {
|
||||
// This series doesn't use spanGaps: true — leave it unchanged.
|
||||
return yValues;
|
||||
}
|
||||
// Replace null with undefined: uPlot skips undefined points without
|
||||
// breaking the line, effectively spanning over the gap.
|
||||
return (yValues as SeriesArray).map((pointValue) =>
|
||||
pointValue === null ? undefined : pointValue,
|
||||
) as uPlot.AlignedData[0];
|
||||
});
|
||||
|
||||
return [newX, ...transformedSeries] as uPlot.AlignedData;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.unauthorized-page {
|
||||
&__description {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,20 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Space, Typography } from 'antd';
|
||||
import UnAuthorized from 'assets/UnAuthorized';
|
||||
import { Container } from 'components/NotFound/styles';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
|
||||
import { useAppContext } from '../../providers/App/App';
|
||||
import { USER_ROLES } from '../../types/roles';
|
||||
|
||||
import './index.styles.scss';
|
||||
import { Button, Container } from 'components/NotFound/styles';
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
function UnAuthorizePage(): JSX.Element {
|
||||
const [debugCurrentRole] = useQueryState('currentRole');
|
||||
const { user } = useAppContext();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const userIsAnonymous =
|
||||
debugCurrentRole === USER_ROLES.ANONYMOUS ||
|
||||
user.role === USER_ROLES.ANONYMOUS;
|
||||
const mistakeMessage = userIsAnonymous
|
||||
? 'If you believe this is a mistake, please contact your administrator or'
|
||||
: 'Please contact your administrator.';
|
||||
|
||||
const handleContactSupportClick = useCallback((): void => {
|
||||
handleContactSupport(isCloudUserVal);
|
||||
}, [isCloudUserVal]);
|
||||
|
||||
return (
|
||||
<Container className="unauthorized-page">
|
||||
<Container>
|
||||
<Space align="center" direction="vertical">
|
||||
<UnAuthorized width={64} height={64} />
|
||||
<Typography.Title level={3}>Access Restricted</Typography.Title>
|
||||
<UnAuthorized />
|
||||
<Typography.Title level={3}>
|
||||
Oops.. you don't have permission to view this page
|
||||
</Typography.Title>
|
||||
|
||||
<p className="unauthorized-page__description">
|
||||
It looks like you don‘t have permission to view this page. <br />
|
||||
{mistakeMessage}
|
||||
{userIsAnonymous ? (
|
||||
<Typography.Link
|
||||
className="contact-support-link"
|
||||
onClick={handleContactSupportClick}
|
||||
>
|
||||
{' '}
|
||||
reach out to us.
|
||||
</Typography.Link>
|
||||
) : null}
|
||||
</p>
|
||||
<Button to={ROUTES.HOME} tabIndex={0} className="periscope-btn primary">
|
||||
Return To Home
|
||||
</Button>
|
||||
</Space>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -19,12 +19,6 @@ import getUserVersion from 'api/v1/version/get';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import dayjs from 'dayjs';
|
||||
import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3';
|
||||
import {
|
||||
IsAdminPermission,
|
||||
IsEditorPermission,
|
||||
IsViewerPermission,
|
||||
} from 'hooks/useAuthZ/legacy';
|
||||
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
|
||||
import { useGetFeatureFlag } from 'hooks/useGetFeatureFlag';
|
||||
import { useGlobalEventListener } from 'hooks/useGlobalEventListener';
|
||||
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
|
||||
@@ -40,7 +34,7 @@ import {
|
||||
UserPreference,
|
||||
} from 'types/api/preferences/preference';
|
||||
import { Organization } from 'types/api/user/getOrganization';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import { IAppContext, IUser } from './types';
|
||||
import { getUserDefaults } from './utils';
|
||||
@@ -49,7 +43,7 @@ export const AppContext = createContext<IAppContext | undefined>(undefined);
|
||||
|
||||
export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
// on load of the provider set the user defaults with access token , refresh token from local storage
|
||||
const [defaultUser, setDefaultUser] = useState<IUser>(() => getUserDefaults());
|
||||
const [user, setUser] = useState<IUser>(() => getUserDefaults());
|
||||
const [activeLicense, setActiveLicense] = useState<LicenseResModel | null>(
|
||||
null,
|
||||
);
|
||||
@@ -76,51 +70,18 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
// if logged out and trying to hit any route none of these calls will trigger
|
||||
const {
|
||||
data: userData,
|
||||
isFetching: isFetchingUserData,
|
||||
error: userFetchDataError,
|
||||
isFetching: isFetchingUser,
|
||||
error: userFetchError,
|
||||
} = useQuery({
|
||||
queryFn: get,
|
||||
queryKey: ['/api/v1/user/me'],
|
||||
enabled: isLoggedIn,
|
||||
});
|
||||
|
||||
const {
|
||||
permissions: permissionsResult,
|
||||
isFetching: isFetchingPermissions,
|
||||
error: errorOnPermissions,
|
||||
refetchPermissions,
|
||||
} = useAuthZ([IsAdminPermission, IsEditorPermission, IsViewerPermission], {
|
||||
enabled: isLoggedIn,
|
||||
});
|
||||
|
||||
const isFetchingUser = isFetchingUserData || isFetchingPermissions;
|
||||
const userFetchError = userFetchDataError || errorOnPermissions;
|
||||
|
||||
const userRole = useMemo(() => {
|
||||
if (permissionsResult?.[IsAdminPermission]?.isGranted) {
|
||||
return USER_ROLES.ADMIN;
|
||||
}
|
||||
if (permissionsResult?.[IsEditorPermission]?.isGranted) {
|
||||
return USER_ROLES.EDITOR;
|
||||
}
|
||||
if (permissionsResult?.[IsViewerPermission]?.isGranted) {
|
||||
return USER_ROLES.VIEWER;
|
||||
}
|
||||
// if none of the permissions, so anonymous
|
||||
return USER_ROLES.ANONYMOUS;
|
||||
}, [permissionsResult]);
|
||||
|
||||
const user: IUser = useMemo(() => {
|
||||
return {
|
||||
...defaultUser,
|
||||
role: userRole as ROLES,
|
||||
};
|
||||
}, [defaultUser, userRole]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetchingUser && userData && userData.data) {
|
||||
setLocalStorageApi(LOCALSTORAGE.LOGGED_IN_USER_EMAIL, userData.data.email);
|
||||
setDefaultUser((prev) => ({
|
||||
setUser((prev) => ({
|
||||
...prev,
|
||||
...userData.data,
|
||||
}));
|
||||
@@ -242,7 +203,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
}, [userPreferencesData, isFetchingUserPreferences, isLoggedIn]);
|
||||
|
||||
function updateUser(user: IUser): void {
|
||||
setDefaultUser((prev) => ({
|
||||
setUser((prev) => ({
|
||||
...prev,
|
||||
...user,
|
||||
}));
|
||||
@@ -283,7 +244,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
...org.slice(orgIndex + 1, org.length),
|
||||
];
|
||||
setOrg(updatedOrg);
|
||||
setDefaultUser((prev) => {
|
||||
setUser((prev) => {
|
||||
if (prev.orgId === orgId) {
|
||||
return {
|
||||
...prev,
|
||||
@@ -311,7 +272,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
// global event listener for AFTER_LOGIN event to start the user fetch post all actions are complete
|
||||
useGlobalEventListener('AFTER_LOGIN', (event) => {
|
||||
if (event.detail) {
|
||||
setDefaultUser((prev) => ({
|
||||
setUser((prev) => ({
|
||||
...prev,
|
||||
accessJwt: event.detail.accessJWT,
|
||||
refreshJwt: event.detail.refreshJWT,
|
||||
@@ -319,14 +280,12 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
}));
|
||||
setIsLoggedIn(true);
|
||||
}
|
||||
|
||||
refetchPermissions();
|
||||
});
|
||||
|
||||
// global event listener for LOGOUT event to clean the app context state
|
||||
useGlobalEventListener('LOGOUT', () => {
|
||||
setIsLoggedIn(false);
|
||||
setDefaultUser(getUserDefaults());
|
||||
setUser(getUserDefaults());
|
||||
setActiveLicense(null);
|
||||
setTrialInfo(null);
|
||||
setFeatureFlags(null);
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import {
|
||||
AuthtypesGettableTransactionDTO,
|
||||
AuthtypesTransactionDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { SINGLE_FLIGHT_WAIT_TIME_MS } from 'hooks/useAuthZ/constants';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import { AppProvider, useAppContext } from '../App';
|
||||
|
||||
const AUTHZ_CHECK_URL = 'http://localhost/api/v1/authz/check';
|
||||
|
||||
jest.mock('constants/env', () => ({
|
||||
ENVIRONMENT: { baseURL: 'http://localhost', wsURL: '' },
|
||||
}));
|
||||
|
||||
/**
|
||||
* Since we are mocking the check permissions, this is needed
|
||||
*/
|
||||
const waitForSinglePreflightToFinish = async (): Promise<void> =>
|
||||
await new Promise((r) => setTimeout(r, SINGLE_FLIGHT_WAIT_TIME_MS));
|
||||
|
||||
function authzMockResponse(
|
||||
payload: AuthtypesTransactionDTO[],
|
||||
authorizedByIndex: boolean[],
|
||||
): { data: AuthtypesGettableTransactionDTO[]; status: string } {
|
||||
return {
|
||||
data: payload.map((txn, i) => ({
|
||||
relation: txn.relation,
|
||||
object: txn.object,
|
||||
authorized: authorizedByIndex[i] ?? false,
|
||||
})),
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function createWrapper(): ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactElement;
|
||||
}) => ReactElement {
|
||||
return function Wrapper({
|
||||
children,
|
||||
}: {
|
||||
children: ReactElement;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppProvider>{children}</AppProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
describe('AppProvider user.role from permissions', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
|
||||
});
|
||||
|
||||
it('sets user.role to ADMIN and hasEditPermission to true when admin permission is granted', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, [true, false, false])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitForSinglePreflightToFinish();
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.user.role).toBe(USER_ROLES.ADMIN);
|
||||
expect(result.current.hasEditPermission).toBe(true);
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('sets user.role to EDITOR and hasEditPermission to true when only editor permission is granted', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, [false, true, false])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitForSinglePreflightToFinish();
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.user.role).toBe(USER_ROLES.EDITOR);
|
||||
expect(result.current.hasEditPermission).toBe(true);
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('sets user.role to VIEWER and hasEditPermission to false when only viewer permission is granted', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, [false, false, true])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitForSinglePreflightToFinish();
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.user.role).toBe(USER_ROLES.VIEWER);
|
||||
expect(result.current.hasEditPermission).toBe(false);
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('sets user.role to ANONYMOUS and hasEditPermission to false when no role permission is granted', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, [false, false, false])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitForSinglePreflightToFinish();
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.user.role).toBe(USER_ROLES.ANONYMOUS);
|
||||
expect(result.current.hasEditPermission).toBe(false);
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* This is expected to not happen, but we'll test it just in case.
|
||||
*/
|
||||
describe('when multiple role permissions are granted', () => {
|
||||
it('prefers ADMIN over EDITOR and VIEWER when multiple role permissions are granted', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, [true, true, true])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.user.role).toBe(USER_ROLES.ADMIN);
|
||||
expect(result.current.hasEditPermission).toBe(true);
|
||||
},
|
||||
{ timeout: 300 },
|
||||
);
|
||||
});
|
||||
|
||||
it('prefers EDITOR over VIEWER when editor and viewer permissions are granted', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(authzMockResponse(payload, [false, true, true])),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitForSinglePreflightToFinish();
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.user.role).toBe(USER_ROLES.EDITOR);
|
||||
expect(result.current.hasEditPermission).toBe(true);
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AppProvider when authz/check fails', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
|
||||
});
|
||||
|
||||
it('sets userFetchError when authz/check returns 500 (same as user fetch error)', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_, res, ctx) =>
|
||||
res(ctx.status(500), ctx.json({ error: 'Internal Server Error' })),
|
||||
),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitForSinglePreflightToFinish();
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.userFetchError).toBeTruthy();
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('sets userFetchError when authz/check fails with network error (same as user fetch error)', async () => {
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, (_, res) => res.networkError('Network error')),
|
||||
);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const { result } = renderHook(() => useAppContext(), { wrapper });
|
||||
|
||||
await waitForSinglePreflightToFinish();
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(result.current.userFetchError).toBeTruthy();
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -141,7 +141,6 @@ export interface IBaseWidget {
|
||||
showPoints?: boolean;
|
||||
lineStyle?: LineStyle;
|
||||
fillMode?: FillMode;
|
||||
spanGaps?: boolean | number;
|
||||
}
|
||||
export interface Widgets extends IBaseWidget {
|
||||
query: Query;
|
||||
|
||||
@@ -13,9 +13,6 @@ export interface UserResponse {
|
||||
displayName: string;
|
||||
orgId: string;
|
||||
organization: string;
|
||||
/**
|
||||
* @deprecated This will be removed in the future releases in favor of new AuthZ framework
|
||||
*/
|
||||
role: ROLES;
|
||||
updatedAt?: number;
|
||||
}
|
||||
|
||||
@@ -2,16 +2,14 @@ export type ADMIN = 'ADMIN';
|
||||
export type VIEWER = 'VIEWER';
|
||||
export type EDITOR = 'EDITOR';
|
||||
export type AUTHOR = 'AUTHOR';
|
||||
export type ANONYMOUS = 'ANONYMOUS';
|
||||
|
||||
export type ROLES = ADMIN | VIEWER | EDITOR | AUTHOR | ANONYMOUS;
|
||||
export type ROLES = ADMIN | VIEWER | EDITOR | AUTHOR;
|
||||
|
||||
export const USER_ROLES = {
|
||||
ADMIN: 'ADMIN',
|
||||
VIEWER: 'VIEWER',
|
||||
EDITOR: 'EDITOR',
|
||||
AUTHOR: 'AUTHOR',
|
||||
ANONYMOUS: 'ANONYMOUS',
|
||||
};
|
||||
|
||||
export enum RoleType {
|
||||
|
||||
@@ -69,7 +69,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
ALERT_OVERVIEW: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
LOGIN: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
FORGOT_PASSWORD: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
NOT_FOUND: ['ADMIN', 'VIEWER', 'EDITOR', 'ANONYMOUS'],
|
||||
NOT_FOUND: ['ADMIN', 'VIEWER', 'EDITOR'],
|
||||
PASSWORD_RESET: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SERVICE_METRICS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SETTINGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
@@ -77,7 +77,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
TRACES_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
TRACE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
TRACE_DETAIL: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
UN_AUTHORIZED: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
|
||||
UN_AUTHORIZED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
USAGE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
VERSION: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
LOGS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
@@ -101,7 +101,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
ROLE_DETAILS: ['ADMIN'],
|
||||
MEMBERS_SETTINGS: ['ADMIN'],
|
||||
BILLING: ['ADMIN'],
|
||||
SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER', 'ANONYMOUS'],
|
||||
SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
LOGS_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
TRACES_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
|
||||
@@ -4506,19 +4506,6 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.1.1"
|
||||
"@radix-ui/react-use-escape-keydown" "1.1.1"
|
||||
|
||||
"@radix-ui/react-dropdown-menu@^2.1.16":
|
||||
version "2.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz#5ee045c62bad8122347981c479d92b1ff24c7254"
|
||||
integrity sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.3"
|
||||
"@radix-ui/react-compose-refs" "1.1.2"
|
||||
"@radix-ui/react-context" "1.1.2"
|
||||
"@radix-ui/react-id" "1.1.1"
|
||||
"@radix-ui/react-menu" "2.1.16"
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.2.2"
|
||||
|
||||
"@radix-ui/react-focus-guards@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz#339c1c69c41628c1a5e655f15f7020bf11aa01fa"
|
||||
@@ -4578,30 +4565,6 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-use-layout-effect" "1.1.1"
|
||||
|
||||
"@radix-ui/react-menu@2.1.16":
|
||||
version "2.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.16.tgz#528a5a973c3a7413d3d49eb9ccd229aa52402911"
|
||||
integrity sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.3"
|
||||
"@radix-ui/react-collection" "1.1.7"
|
||||
"@radix-ui/react-compose-refs" "1.1.2"
|
||||
"@radix-ui/react-context" "1.1.2"
|
||||
"@radix-ui/react-direction" "1.1.1"
|
||||
"@radix-ui/react-dismissable-layer" "1.1.11"
|
||||
"@radix-ui/react-focus-guards" "1.1.3"
|
||||
"@radix-ui/react-focus-scope" "1.1.7"
|
||||
"@radix-ui/react-id" "1.1.1"
|
||||
"@radix-ui/react-popper" "1.2.8"
|
||||
"@radix-ui/react-portal" "1.1.9"
|
||||
"@radix-ui/react-presence" "1.1.5"
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
"@radix-ui/react-roving-focus" "1.1.11"
|
||||
"@radix-ui/react-slot" "1.2.3"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.1"
|
||||
aria-hidden "^1.2.4"
|
||||
react-remove-scroll "^2.6.3"
|
||||
|
||||
"@radix-ui/react-popover@^1.1.15", "@radix-ui/react-popover@^1.1.2":
|
||||
version "1.1.15"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.15.tgz#9c852f93990a687ebdc949b2c3de1f37cdc4c5d5"
|
||||
@@ -4841,20 +4804,6 @@
|
||||
"@radix-ui/react-roving-focus" "1.0.4"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
|
||||
"@radix-ui/react-tabs@^1.1.3":
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz#3537ce379d7e7ff4eeb6b67a0973e139c2ac1f15"
|
||||
integrity sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==
|
||||
dependencies:
|
||||
"@radix-ui/primitive" "1.1.3"
|
||||
"@radix-ui/react-context" "1.1.2"
|
||||
"@radix-ui/react-direction" "1.1.1"
|
||||
"@radix-ui/react-id" "1.1.1"
|
||||
"@radix-ui/react-presence" "1.1.5"
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
"@radix-ui/react-roving-focus" "1.1.11"
|
||||
"@radix-ui/react-use-controllable-state" "1.2.2"
|
||||
|
||||
"@radix-ui/react-toggle-group@^1.1.7":
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz#e513d6ffdb07509b400ab5b26f2523747c0d51c1"
|
||||
@@ -5726,42 +5675,6 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/ui@0.0.5":
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/ui/-/ui-0.0.5.tgz#8badef53416b7ace0fe61ff01ff3da679a0e4ba5"
|
||||
integrity sha512-4vPvUh3rwpst068qXUZ26JfCQGv1vo1xMSwtKw6wTjiiq1Bf3geP84HWVXycNMIrIeVnUgDGnqe0D4doh+mL8A==
|
||||
dependencies:
|
||||
"@radix-ui/react-checkbox" "^1.2.3"
|
||||
"@radix-ui/react-dialog" "^1.1.11"
|
||||
"@radix-ui/react-dropdown-menu" "^2.1.16"
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-popover" "^1.1.15"
|
||||
"@radix-ui/react-radio-group" "^1.3.4"
|
||||
"@radix-ui/react-slot" "^1.2.3"
|
||||
"@radix-ui/react-switch" "^1.1.4"
|
||||
"@radix-ui/react-tabs" "^1.1.3"
|
||||
"@radix-ui/react-toggle" "^1.1.6"
|
||||
"@radix-ui/react-toggle-group" "^1.1.7"
|
||||
"@radix-ui/react-tooltip" "^1.2.6"
|
||||
"@tanstack/react-table" "^8.21.3"
|
||||
"@tanstack/react-virtual" "^3.13.9"
|
||||
"@types/lodash-es" "^4.17.12"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
cmdk "^1.1.1"
|
||||
date-fns "^4.1.0"
|
||||
dayjs "^1.11.10"
|
||||
lodash-es "^4.17.21"
|
||||
lucide-react "^0.445.0"
|
||||
lucide-solid "^0.510.0"
|
||||
motion "^11.11.17"
|
||||
next-themes "^0.4.6"
|
||||
nuqs "^2.8.9"
|
||||
react-day-picker "^9.8.1"
|
||||
react-resizable-panels "^4.7.1"
|
||||
sonner "^2.0.7"
|
||||
tailwind-merge "^3.5.0"
|
||||
|
||||
"@sinclair/typebox@^0.25.16":
|
||||
version "0.25.24"
|
||||
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz"
|
||||
@@ -9660,11 +9573,6 @@ dayjs@^1.10.7, dayjs@^1.11.1:
|
||||
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz"
|
||||
integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
|
||||
|
||||
dayjs@^1.11.10:
|
||||
version "1.11.20"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938"
|
||||
integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==
|
||||
|
||||
debounce@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
|
||||
@@ -11184,15 +11092,6 @@ fraction.js@^4.3.7:
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||
|
||||
framer-motion@^11.18.2:
|
||||
version "11.18.2"
|
||||
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.18.2.tgz#0c6bd05677f4cfd3b3bdead4eb5ecdd5ed245718"
|
||||
integrity sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==
|
||||
dependencies:
|
||||
motion-dom "^11.18.1"
|
||||
motion-utils "^11.18.1"
|
||||
tslib "^2.4.0"
|
||||
|
||||
framer-motion@^12.4.13:
|
||||
version "12.4.13"
|
||||
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.4.13.tgz#1efd954f95e6a54685b660929c00f5a61e35256a"
|
||||
@@ -15103,13 +15002,6 @@ moment@^2.29.4:
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||
|
||||
motion-dom@^11.18.1:
|
||||
version "11.18.1"
|
||||
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-11.18.1.tgz#e7fed7b7dc6ae1223ef1cce29ee54bec826dc3f2"
|
||||
integrity sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==
|
||||
dependencies:
|
||||
motion-utils "^11.18.1"
|
||||
|
||||
motion-dom@^12.4.11:
|
||||
version "12.4.11"
|
||||
resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.4.11.tgz#0419c8686cda4d523f08249deeb8fa6683a9b9d3"
|
||||
@@ -15117,11 +15009,6 @@ motion-dom@^12.4.11:
|
||||
dependencies:
|
||||
motion-utils "^12.4.10"
|
||||
|
||||
motion-utils@^11.18.1:
|
||||
version "11.18.1"
|
||||
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-11.18.1.tgz#671227669833e991c55813cf337899f41327db5b"
|
||||
integrity sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==
|
||||
|
||||
motion-utils@^12.4.10:
|
||||
version "12.4.10"
|
||||
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.4.10.tgz#3d93acea5454419eaaad8d5e5425cb71cbfa1e7f"
|
||||
@@ -15135,14 +15022,6 @@ motion@12.4.13:
|
||||
framer-motion "^12.4.13"
|
||||
tslib "^2.4.0"
|
||||
|
||||
motion@^11.11.17:
|
||||
version "11.18.2"
|
||||
resolved "https://registry.yarnpkg.com/motion/-/motion-11.18.2.tgz#17fb372f3ed94fc9ee1384a25a9068e9da1951e7"
|
||||
integrity sha512-JLjvFDuFr42NFtcVoMAyC2sEjnpA8xpy6qWPyzQvCloznAyQ8FIXioxWfHiLtgYhoVpfUqSWpn1h9++skj9+Wg==
|
||||
dependencies:
|
||||
framer-motion "^11.18.2"
|
||||
tslib "^2.4.0"
|
||||
|
||||
mri@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||
@@ -15413,13 +15292,6 @@ nuqs@2.8.8:
|
||||
dependencies:
|
||||
"@standard-schema/spec" "1.0.0"
|
||||
|
||||
nuqs@^2.8.9:
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/nuqs/-/nuqs-2.8.9.tgz#e2c27d87c0dd0e3b4412fe867bcd0947cc4c998f"
|
||||
integrity sha512-8ou6AEwsxMWSYo2qkfZtYFVzngwbKmg4c00HVxC1fF6CEJv3Fwm6eoZmfVPALB+vw8Udo7KL5uy96PFcYe1BIQ==
|
||||
dependencies:
|
||||
"@standard-schema/spec" "1.0.0"
|
||||
|
||||
nwsapi@^2.2.2:
|
||||
version "2.2.23"
|
||||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.23.tgz#59712c3a88e6de2bb0b6ccc1070397267019cf6c"
|
||||
@@ -17085,11 +16957,6 @@ react-resizable-panels@^3.0.5:
|
||||
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-3.0.5.tgz#50a20645263eed02344de4a70d1319bbc0014bbd"
|
||||
integrity sha512-3z1yN25DMTXLg2wfyFrW32r5k4WEcUa3F7cJ2EgtNK07lnOs4mpM8yWLGunCpkhcQRwJX4fqoLcIh/pHPxzlmQ==
|
||||
|
||||
react-resizable-panels@^4.7.1:
|
||||
version "4.7.3"
|
||||
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-4.7.3.tgz#4040aa0f5c5c4cc4bb685cb69973601ccda3b014"
|
||||
integrity sha512-PYcYMLtvJD+Pr0TQNeMvddcnLOwUa/Yb4iNwU7ThNLlHaQYEEC9MIBWHaBGODzYuXIkPRZ/OWe5sbzG1Rzq5ew==
|
||||
|
||||
react-resizable@3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.4.tgz"
|
||||
@@ -18930,11 +18797,6 @@ tailwind-merge@^2.5.2:
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5"
|
||||
integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==
|
||||
|
||||
tailwind-merge@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz#06502f4496ba15151445d97d916a26564d50d1ca"
|
||||
integrity sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==
|
||||
|
||||
tailwindcss-animate@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"
|
||||
|
||||
2
go.mod
2
go.mod
@@ -11,6 +11,7 @@ require (
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.2
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
github.com/bytedance/sonic v1.14.1
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0
|
||||
@@ -105,7 +106,6 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func (provider *provider) addGatewayRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.GetIngestionKeys), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys", handler.New(provider.authZ.AdminAccess(provider.gatewayHandler.GetIngestionKeys), handler.OpenAPIDef{
|
||||
ID: "GetIngestionKeys",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Get ingestion keys for workspace",
|
||||
@@ -23,12 +23,12 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/search", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.SearchIngestionKeys), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/search", handler.New(provider.authZ.AdminAccess(provider.gatewayHandler.SearchIngestionKeys), handler.OpenAPIDef{
|
||||
ID: "SearchIngestionKeys",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Search ingestion keys for workspace",
|
||||
@@ -41,12 +41,12 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.CreateIngestionKey), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys", handler.New(provider.authZ.AdminAccess(provider.gatewayHandler.CreateIngestionKey), handler.OpenAPIDef{
|
||||
ID: "CreateIngestionKey",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Create ingestion key for workspace",
|
||||
@@ -58,12 +58,12 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.UpdateIngestionKey), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authZ.AdminAccess(provider.gatewayHandler.UpdateIngestionKey), handler.OpenAPIDef{
|
||||
ID: "UpdateIngestionKey",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Update ingestion key for workspace",
|
||||
@@ -75,12 +75,12 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.DeleteIngestionKey), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}", handler.New(provider.authZ.AdminAccess(provider.gatewayHandler.DeleteIngestionKey), handler.OpenAPIDef{
|
||||
ID: "DeleteIngestionKey",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Delete ingestion key for workspace",
|
||||
@@ -92,12 +92,12 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}/limits", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.CreateIngestionKeyLimit), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/{keyId}/limits", handler.New(provider.authZ.AdminAccess(provider.gatewayHandler.CreateIngestionKeyLimit), handler.OpenAPIDef{
|
||||
ID: "CreateIngestionKeyLimit",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Create limit for the ingestion key",
|
||||
@@ -109,12 +109,12 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.UpdateIngestionKeyLimit), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authZ.AdminAccess(provider.gatewayHandler.UpdateIngestionKeyLimit), handler.OpenAPIDef{
|
||||
ID: "UpdateIngestionKeyLimit",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Update limit for the ingestion key",
|
||||
@@ -126,12 +126,12 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authZ.EditAccess(provider.gatewayHandler.DeleteIngestionKeyLimit), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/gateway/ingestion_keys/limits/{limitId}", handler.New(provider.authZ.AdminAccess(provider.gatewayHandler.DeleteIngestionKeyLimit), handler.OpenAPIDef{
|
||||
ID: "DeleteIngestionKeyLimit",
|
||||
Tags: []string{"gateway"},
|
||||
Summary: "Delete limit for the ingestion key",
|
||||
@@ -143,7 +143,7 @@ func (provider *provider) addGatewayRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,24 +4,24 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types/globaltypes"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addGlobalRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/global/config", handler.New(provider.authZ.OpenAccess(provider.globalHandler.GetConfig), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v1/global/config", handler.New(provider.authZ.EditAccess(provider.globalHandler.GetConfig), handler.OpenAPIDef{
|
||||
ID: "GetGlobalConfig",
|
||||
Tags: []string{"global"},
|
||||
Summary: "Get global config",
|
||||
Description: "This endpoint returns global config",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(globaltypes.Config),
|
||||
Response: new(types.GettableGlobalConfig),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: nil,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
|
||||
func newSecuritySchemes(role types.Role) []handler.OpenAPISecurityScheme {
|
||||
return []handler.OpenAPISecurityScheme{
|
||||
{Name: authtypes.IdentNProviderAPIKey.StringValue(), Scopes: []string{role.String()}},
|
||||
{Name: authtypes.IdentNProviderAPIkey.StringValue(), Scopes: []string{role.String()}},
|
||||
{Name: authtypes.IdentNProviderTokenizer.StringValue(), Scopes: []string{role.String()}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint lists all users",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: make([]*types.User, 0),
|
||||
Response: make([]*types.GettableUser, 0),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
@@ -203,7 +203,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint returns the user I belong to",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(types.User),
|
||||
Response: new(types.GettableUser),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
@@ -220,7 +220,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint returns the user by id",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(types.User),
|
||||
Response: new(types.GettableUser),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
@@ -237,7 +237,7 @@ func (provider *provider) addUserRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint updates the user by id",
|
||||
Request: new(types.User),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(types.User),
|
||||
Response: new(types.GettableUser),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
|
||||
@@ -17,8 +17,8 @@ func NewStore(sqlstore sqlstore.SQLStore) authtypes.AuthNStore {
|
||||
return &store{sqlstore: sqlstore}
|
||||
}
|
||||
|
||||
func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.StorableUser, *types.FactorPassword, error) {
|
||||
user := new(types.StorableUser)
|
||||
func (store *store) GetActiveUserAndFactorPasswordByEmailAndOrgID(ctx context.Context, email string, orgID valuer.UUID) (*types.User, *types.FactorPassword, error) {
|
||||
user := new(types.User)
|
||||
factorPassword := new(types.FactorPassword)
|
||||
|
||||
err := store.
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/globaltypes"
|
||||
)
|
||||
import "net/http"
|
||||
|
||||
type Global interface {
|
||||
GetConfig(context.Context) *globaltypes.Config
|
||||
GetConfig() Config
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package signozglobal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
@@ -18,10 +17,7 @@ func NewHandler(global global.Global) global.Handler {
|
||||
}
|
||||
|
||||
func (handler *handler) GetConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
cfg := handler.global.GetConfig()
|
||||
|
||||
cfg := handler.global.GetConfig(ctx)
|
||||
|
||||
render.Success(rw, http.StatusOK, cfg)
|
||||
render.Success(rw, http.StatusOK, types.NewGettableGlobalConfig(cfg.ExternalURL, cfg.IngestionURL))
|
||||
}
|
||||
|
||||
@@ -5,38 +5,27 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/global"
|
||||
"github.com/SigNoz/signoz/pkg/identn"
|
||||
"github.com/SigNoz/signoz/pkg/types/globaltypes"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
config global.Config
|
||||
identNConfig identn.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
config global.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
}
|
||||
|
||||
func NewFactory(identNConfig identn.Config) factory.ProviderFactory[global.Global, global.Config] {
|
||||
func NewFactory() factory.ProviderFactory[global.Global, global.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config global.Config) (global.Global, error) {
|
||||
return newProvider(ctx, providerSettings, config, identNConfig)
|
||||
return newProvider(ctx, providerSettings, config)
|
||||
})
|
||||
}
|
||||
|
||||
func newProvider(_ context.Context, providerSettings factory.ProviderSettings, config global.Config, identNConfig identn.Config) (global.Global, error) {
|
||||
func newProvider(_ context.Context, providerSettings factory.ProviderSettings, config global.Config) (global.Global, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/global/signozglobal")
|
||||
return &provider{
|
||||
config: config,
|
||||
identNConfig: identNConfig,
|
||||
settings: settings,
|
||||
config: config,
|
||||
settings: settings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) GetConfig(context.Context) *globaltypes.Config {
|
||||
return globaltypes.NewConfig(
|
||||
globaltypes.NewEndpoint(provider.config.ExternalURL.String(), provider.config.IngestionURL.String()),
|
||||
globaltypes.NewIdentNConfig(
|
||||
globaltypes.TokenizerConfig{Enabled: provider.identNConfig.Tokenizer.Enabled},
|
||||
globaltypes.APIKeyConfig{Enabled: provider.identNConfig.APIKeyConfig.Enabled},
|
||||
globaltypes.ImpersonationConfig{Enabled: provider.identNConfig.Impersonation.Enabled},
|
||||
),
|
||||
)
|
||||
func (provider *provider) GetConfig() global.Config {
|
||||
return provider.config
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"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/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -40,7 +41,9 @@ func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if claims.IdentNProvider == authtypes.IdentNProviderAPIKey.StringValue() {
|
||||
commentCtx := ctxtypes.CommentFromContext(ctx)
|
||||
authtype, ok := commentCtx.Map()["auth_type"]
|
||||
if ok && (authtype == authtypes.IdentNProviderAPIkey.StringValue()) {
|
||||
if err := claims.IsViewer(); err != nil {
|
||||
middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims)
|
||||
render.Error(rw, err)
|
||||
@@ -90,7 +93,9 @@ func (middleware *AuthZ) EditAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if claims.IdentNProvider == authtypes.IdentNProviderAPIKey.StringValue() {
|
||||
commentCtx := ctxtypes.CommentFromContext(ctx)
|
||||
authtype, ok := commentCtx.Map()["auth_type"]
|
||||
if ok && (authtype == authtypes.IdentNProviderAPIkey.StringValue()) {
|
||||
if err := claims.IsEditor(); err != nil {
|
||||
middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims)
|
||||
render.Error(rw, err)
|
||||
@@ -139,7 +144,9 @@ func (middleware *AuthZ) AdminAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if claims.IdentNProvider == authtypes.IdentNProviderAPIKey.StringValue() {
|
||||
commentCtx := ctxtypes.CommentFromContext(ctx)
|
||||
authtype, ok := commentCtx.Map()["auth_type"]
|
||||
if ok && (authtype == authtypes.IdentNProviderAPIkey.StringValue()) {
|
||||
if err := claims.IsAdmin(); err != nil {
|
||||
middleware.logger.WarnContext(ctx, authzDeniedMessage, "claims", claims)
|
||||
render.Error(rw, err)
|
||||
|
||||
@@ -25,7 +25,7 @@ type provider struct {
|
||||
}
|
||||
|
||||
func NewFactory(store sqlstore.SQLStore) factory.ProviderFactory[identn.IdentN, identn.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName(authtypes.IdentNProviderAPIKey.StringValue()), func(ctx context.Context, providerSettings factory.ProviderSettings, config identn.Config) (identn.IdentN, error) {
|
||||
return factory.NewProviderFactory(factory.MustNewName(authtypes.IdentNProviderAPIkey.StringValue()), func(ctx context.Context, providerSettings factory.ProviderSettings, config identn.Config) (identn.IdentN, error) {
|
||||
return New(providerSettings, store, config)
|
||||
})
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func New(providerSettings factory.ProviderSettings, store sqlstore.SQLStore, con
|
||||
}
|
||||
|
||||
func (provider *provider) Name() authtypes.IdentNProvider {
|
||||
return authtypes.IdentNProviderAPIKey
|
||||
return authtypes.IdentNProviderAPIkey
|
||||
}
|
||||
|
||||
func (provider *provider) Test(req *http.Request) bool {
|
||||
@@ -52,6 +52,10 @@ func (provider *provider) Test(req *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (provider *provider) Enabled() bool {
|
||||
return provider.config.APIKeyConfig.Enabled
|
||||
}
|
||||
|
||||
func (provider *provider) Pre(req *http.Request) *http.Request {
|
||||
token := provider.extractToken(req)
|
||||
if token == "" {
|
||||
@@ -85,7 +89,7 @@ func (provider *provider) GetIdentity(req *http.Request) (*authtypes.Identity, e
|
||||
return nil, errors.New(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "api key has expired")
|
||||
}
|
||||
|
||||
var user types.StorableUser
|
||||
var user types.User
|
||||
err = provider.
|
||||
store.
|
||||
BunDB().
|
||||
@@ -97,8 +101,13 @@ func (provider *provider) GetIdentity(req *http.Request) (*authtypes.Identity, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identity := authtypes.NewIdentity(user.ID, user.OrgID, user.Email, apiKey.Role, provider.Name())
|
||||
return identity, nil
|
||||
identity := authtypes.Identity{
|
||||
UserID: user.ID,
|
||||
Role: apiKey.Role,
|
||||
Email: user.Email,
|
||||
OrgID: user.OrgID,
|
||||
}
|
||||
return &identity, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Post(ctx context.Context, _ *http.Request, _ authtypes.Claims) {
|
||||
@@ -1,7 +1,6 @@
|
||||
package identn
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
)
|
||||
|
||||
@@ -11,20 +10,11 @@ type Config struct {
|
||||
|
||||
// Config for apikey identN resolver
|
||||
APIKeyConfig APIKeyConfig `mapstructure:"apikey"`
|
||||
|
||||
// Config for impersonation identN resolver
|
||||
Impersonation ImpersonationConfig `mapstructure:"impersonation"`
|
||||
}
|
||||
|
||||
type ImpersonationConfig struct {
|
||||
// Toggles the identN resolver
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
}
|
||||
|
||||
type TokenizerConfig struct {
|
||||
// Toggles the identN resolver
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
|
||||
// Headers to extract from incoming requests
|
||||
Headers []string `mapstructure:"headers"`
|
||||
}
|
||||
@@ -32,7 +22,6 @@ type TokenizerConfig struct {
|
||||
type APIKeyConfig struct {
|
||||
// Toggles the identN resolver
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
|
||||
// Headers to extract from incoming requests
|
||||
Headers []string `mapstructure:"headers"`
|
||||
}
|
||||
@@ -51,22 +40,9 @@ func newConfig() factory.Config {
|
||||
Enabled: true,
|
||||
Headers: []string{"SIGNOZ-API-KEY"},
|
||||
},
|
||||
Impersonation: ImpersonationConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if c.Impersonation.Enabled {
|
||||
if c.Tokenizer.Enabled {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "identn::impersonation cannot be enabled if identn::tokenizer is enabled")
|
||||
}
|
||||
|
||||
if c.APIKeyConfig.Enabled {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "identn::impersonation cannot be enabled if identn::apikey is enabled")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ type IdentN interface {
|
||||
GetIdentity(r *http.Request) (*authtypes.Identity, error)
|
||||
|
||||
Name() authtypes.IdentNProvider
|
||||
|
||||
Enabled() bool
|
||||
}
|
||||
|
||||
// IdentNWithPreHook is optionally implemented by resolvers that need to
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package impersonationidentn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/identn"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
config identn.Config
|
||||
settings factory.ScopedProviderSettings
|
||||
orgGetter organization.Getter
|
||||
userGetter user.Getter
|
||||
userConfig user.Config
|
||||
|
||||
mu sync.RWMutex
|
||||
identity *authtypes.Identity
|
||||
}
|
||||
|
||||
func NewFactory(orgGetter organization.Getter, userGetter user.Getter, userConfig user.Config) factory.ProviderFactory[identn.IdentN, identn.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName(authtypes.IdentNProviderImpersonation.StringValue()), func(ctx context.Context, providerSettings factory.ProviderSettings, config identn.Config) (identn.IdentN, error) {
|
||||
return New(ctx, providerSettings, config, orgGetter, userGetter, userConfig)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, providerSettings factory.ProviderSettings, config identn.Config, orgGetter organization.Getter, userGetter user.Getter, userConfig user.Config) (identn.IdentN, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/identn/impersonationidentn")
|
||||
|
||||
settings.Logger().WarnContext(ctx, "impersonation identity provider is enabled, all requests will impersonate the root user")
|
||||
|
||||
if !userConfig.Root.Enabled {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "root user is not enabled, impersonation identity provider will not be able to resolve any identity")
|
||||
}
|
||||
|
||||
return &provider{
|
||||
config: config,
|
||||
settings: settings,
|
||||
orgGetter: orgGetter,
|
||||
userGetter: userGetter,
|
||||
userConfig: userConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Name() authtypes.IdentNProvider {
|
||||
return authtypes.IdentNProviderImpersonation
|
||||
}
|
||||
|
||||
func (provider *provider) Test(_ *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (provider *provider) GetIdentity(req *http.Request) (*authtypes.Identity, error) {
|
||||
ctx := req.Context()
|
||||
|
||||
provider.mu.RLock()
|
||||
if provider.identity != nil {
|
||||
provider.mu.RUnlock()
|
||||
return provider.identity, nil
|
||||
}
|
||||
provider.mu.RUnlock()
|
||||
|
||||
provider.mu.Lock()
|
||||
defer provider.mu.Unlock()
|
||||
|
||||
// Re-check after acquiring write lock; another goroutine may have resolved it.
|
||||
if provider.identity != nil {
|
||||
return provider.identity, nil
|
||||
}
|
||||
|
||||
org, _, err := provider.orgGetter.GetByIDOrName(ctx, provider.userConfig.Root.Org.ID, provider.userConfig.Root.Org.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootUser, err := provider.userGetter.GetRootUserByOrgID(ctx, org.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provider.identity = authtypes.NewIdentity(
|
||||
rootUser.ID,
|
||||
rootUser.OrgID,
|
||||
rootUser.Email,
|
||||
rootUser.Role,
|
||||
authtypes.IdentNProviderImpersonation,
|
||||
)
|
||||
|
||||
return provider.identity, nil
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
package identn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
)
|
||||
|
||||
type identNResolver struct {
|
||||
@@ -13,55 +11,19 @@ type identNResolver struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
}
|
||||
|
||||
func NewIdentNResolver(ctx context.Context, providerSettings factory.ProviderSettings, identNConfig Config, identNFactories factory.NamedMap[factory.ProviderFactory[IdentN, Config]]) (IdentNResolver, error) {
|
||||
identNs := []IdentN{}
|
||||
func NewIdentNResolver(providerSettings factory.ProviderSettings, identNs ...IdentN) IdentNResolver {
|
||||
enabledIdentNs := []IdentN{}
|
||||
|
||||
if identNConfig.Impersonation.Enabled {
|
||||
identNFactory, err := identNFactories.Get(authtypes.IdentNProviderImpersonation.StringValue())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for _, identN := range identNs {
|
||||
if identN.Enabled() {
|
||||
enabledIdentNs = append(enabledIdentNs, identN)
|
||||
}
|
||||
|
||||
identN, err := identNFactory.New(ctx, providerSettings, identNConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identNs = append(identNs, identN)
|
||||
}
|
||||
|
||||
if identNConfig.Tokenizer.Enabled {
|
||||
identNFactory, err := identNFactories.Get(authtypes.IdentNProviderTokenizer.StringValue())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identN, err := identNFactory.New(ctx, providerSettings, identNConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identNs = append(identNs, identN)
|
||||
}
|
||||
|
||||
if identNConfig.APIKeyConfig.Enabled {
|
||||
identNFactory, err := identNFactories.Get(authtypes.IdentNProviderAPIKey.StringValue())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identN, err := identNFactory.New(ctx, providerSettings, identNConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
identNs = append(identNs, identN)
|
||||
}
|
||||
|
||||
return &identNResolver{
|
||||
identNs: identNs,
|
||||
identNs: enabledIdentNs,
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/identn"),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetIdentN returns the first IdentN whose Test() returns true.
|
||||
|
||||
@@ -48,6 +48,10 @@ func (provider *provider) Test(req *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (provider *provider) Enabled() bool {
|
||||
return provider.config.Tokenizer.Enabled
|
||||
}
|
||||
|
||||
func (provider *provider) Pre(req *http.Request) *http.Request {
|
||||
accessToken := provider.extractToken(req)
|
||||
if accessToken == "" {
|
||||
@@ -3,7 +3,6 @@ package implorganization
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/sharder"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
@@ -23,33 +22,6 @@ func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.Organizat
|
||||
return module.store.Get(ctx, id)
|
||||
}
|
||||
|
||||
func (module *getter) GetByIDOrName(ctx context.Context, id valuer.UUID, name string) (*types.Organization, bool, error) {
|
||||
if id.IsZero() {
|
||||
org, err := module.store.GetByName(ctx, name)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return org, true, nil
|
||||
}
|
||||
|
||||
org, err := module.store.Get(ctx, id)
|
||||
if err == nil {
|
||||
return org, false, nil
|
||||
}
|
||||
|
||||
if !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
org, err = module.store.GetByName(ctx, name)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return org, true, nil
|
||||
}
|
||||
|
||||
func (module *getter) ListByOwnedKeyRange(ctx context.Context) ([]*types.Organization, error) {
|
||||
start, end, err := module.sharder.GetMyOwnedKeyRange(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -12,10 +12,6 @@ type Getter interface {
|
||||
// Get gets the organization based on the given id
|
||||
Get(context.Context, valuer.UUID) (*types.Organization, error)
|
||||
|
||||
// GetByIDOrName gets the organization by id, falling back to name on not found.
|
||||
// The boolean is true when the name fallback path was used.
|
||||
GetByIDOrName(context.Context, valuer.UUID, string) (*types.Organization, bool, error)
|
||||
|
||||
// ListByOwnedKeyRange gets all the organizations owned by the instance
|
||||
ListByOwnedKeyRange(context.Context) ([]*types.Organization, error)
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ func (m *module) ListPromotedAndIndexedPaths(ctx context.Context) ([]promotetype
|
||||
|
||||
// add the paths that are not promoted but have indexes
|
||||
for path, indexes := range aggr {
|
||||
path := strings.TrimPrefix(path, telemetrylogs.BodyV2ColumnPrefix)
|
||||
path := strings.TrimPrefix(path, telemetrylogs.BodyJSONColumnPrefix)
|
||||
path = telemetrytypes.BodyJSONStringSearchPrefix + path
|
||||
response = append(response, promotetypes.PromotePath{
|
||||
Path: path,
|
||||
@@ -163,7 +163,7 @@ func (m *module) PromoteAndIndexPaths(
|
||||
}
|
||||
}
|
||||
if len(it.Indexes) > 0 {
|
||||
parentColumn := telemetrylogs.LogsV2BodyV2Column
|
||||
parentColumn := telemetrylogs.LogsV2BodyJSONColumn
|
||||
// if the path is already promoted or is being promoted, add it to the promoted column
|
||||
if _, promoted := existingPromotedPaths[it.Path]; promoted || it.Promote {
|
||||
parentColumn = telemetrylogs.LogsV2BodyPromotedColumn
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
@@ -29,10 +28,9 @@ type module struct {
|
||||
authDomain authdomain.Module
|
||||
tokenizer tokenizer.Tokenizer
|
||||
orgGetter organization.Getter
|
||||
authz authz.AuthZ
|
||||
}
|
||||
|
||||
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, user user.Module, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter, authz authz.AuthZ) session.Module {
|
||||
func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.AuthNProvider]authn.AuthN, user user.Module, userGetter user.Getter, authDomain authdomain.Module, tokenizer tokenizer.Tokenizer, orgGetter organization.Getter) session.Module {
|
||||
return &module{
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/session/implsession"),
|
||||
authNs: authNs,
|
||||
@@ -41,7 +39,6 @@ func NewModule(providerSettings factory.ProviderSettings, authNs map[authtypes.A
|
||||
authDomain: authDomain,
|
||||
tokenizer: tokenizer,
|
||||
orgGetter: orgGetter,
|
||||
authz: authz,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,16 +142,9 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi
|
||||
}
|
||||
|
||||
roleMapping := authDomain.AuthDomainConfig().RoleMapping
|
||||
managedRoles := roleMapping.ManagedRolesFromCallbackIdentity(callbackIdentity)
|
||||
role := roleMapping.NewRoleFromCallbackIdentity(callbackIdentity)
|
||||
|
||||
// pass only valid or fallback to viewer
|
||||
validRoles, err := module.resolveValidRoles(ctx, callbackIdentity.OrgID, managedRoles, callbackIdentity.Email)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
legacyRole := authtypes.HighestLegacyRoleFromManagedRoles(validRoles)
|
||||
user, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, legacyRole, validRoles, callbackIdentity.OrgID, types.UserStatusActive)
|
||||
user, err := types.NewUser(callbackIdentity.Name, callbackIdentity.Email, role, callbackIdentity.OrgID, types.UserStatusActive)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -232,34 +222,3 @@ func getProvider[T authn.AuthN](authNProvider authtypes.AuthNProvider, authNs ma
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// resolveValidRoles validates role names against the database
|
||||
// returns only roles that exist. If none are valid, falls back to signoz-viewer role
|
||||
func (module *module) resolveValidRoles(ctx context.Context, orgID valuer.UUID, roles []string, email valuer.Email) ([]string, error) {
|
||||
validRoles := make([]string, 0, len(roles))
|
||||
var ignored []string
|
||||
|
||||
for _, roleName := range roles {
|
||||
_, err := module.authz.GetByOrgIDAndName(ctx, orgID, roleName)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
ignored = append(ignored, roleName)
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
validRoles = append(validRoles, roleName)
|
||||
}
|
||||
|
||||
if len(ignored) > 0 {
|
||||
module.settings.Logger().WarnContext(ctx, "ignoring non-existent roles from SSO mapping", "ignored_roles", ignored, "email", email)
|
||||
}
|
||||
|
||||
// fallback to viewer if no valid roles
|
||||
if len(validRoles) == 0 {
|
||||
module.settings.Logger().WarnContext(ctx, "no valid roles from SSO mapping, falling back to viewer", "email", email)
|
||||
validRoles = []string{authtypes.SigNozViewerRoleName}
|
||||
}
|
||||
|
||||
return validRoles, nil
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (module *module) Create(ctx context.Context, timestamp int64, name string,
|
||||
funnel.CreatedBy = userID.String()
|
||||
|
||||
// Set up the user relationship
|
||||
funnel.CreatedByUser = &types.StorableUser{
|
||||
funnel.CreatedByUser = &types.User{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: userID,
|
||||
},
|
||||
|
||||
@@ -4,108 +4,76 @@ import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"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/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type getter struct {
|
||||
store types.UserStore
|
||||
authz authz.AuthZ
|
||||
userRoleStore authtypes.UserRoleStore
|
||||
flagger flagger.Flagger
|
||||
store types.UserStore
|
||||
flagger flagger.Flagger
|
||||
}
|
||||
|
||||
func NewGetter(store types.UserStore, authz authz.AuthZ, userRoleStore authtypes.UserRoleStore, flagger flagger.Flagger) user.Getter {
|
||||
return &getter{store: store, authz: authz, userRoleStore: userRoleStore, flagger: flagger}
|
||||
func NewGetter(store types.UserStore, flagger flagger.Flagger) user.Getter {
|
||||
return &getter{store: store, flagger: flagger}
|
||||
}
|
||||
|
||||
func (module *getter) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.User, error) {
|
||||
storableUser, err := module.store.GetRootUserByOrgID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleNames, err := module.resolveRoleNamesForUser(ctx, storableUser.ID, storableUser.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := types.NewUserFromStorable(storableUser, roleNames)
|
||||
|
||||
return user, nil
|
||||
return module.store.GetRootUserByOrgID(ctx, orgID)
|
||||
}
|
||||
|
||||
func (module *getter) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.User, error) {
|
||||
storableUsers, err := module.store.ListUsersByOrgID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userIDs := make([]valuer.UUID, len(storableUsers))
|
||||
for idx, storableUser := range storableUsers {
|
||||
userIDs[idx] = storableUser.ID
|
||||
}
|
||||
|
||||
storableUserRoles, err := module.userRoleStore.ListUserRolesByOrgIDAndUserIDs(ctx, orgID, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userIDToRoleIDs, roleIDs := authtypes.GetUserIDToRoleIDsMappingAndUniqueRoles(storableUserRoles)
|
||||
roles, err := module.authz.ListByOrgIDAndIDs(ctx, orgID, roleIDs)
|
||||
users, err := module.store.ListUsersByOrgID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// filter root users if feature flag `hide_root_users` is true
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
||||
hideRootUsers := module.flagger.BooleanOrEmpty(ctx, flagger.FeatureHideRootUser, evalCtx)
|
||||
|
||||
if hideRootUsers {
|
||||
storableUsers = slices.DeleteFunc(storableUsers, func(user *types.StorableUser) bool { return user.IsRoot })
|
||||
users = slices.DeleteFunc(users, func(user *types.User) bool { return user.IsRoot })
|
||||
}
|
||||
|
||||
users := module.usersFromStorableUsersAndRolesMaps(storableUsers, roles, userIDToRoleIDs)
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (module *getter) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*types.User, error) {
|
||||
users, err := module.store.GetUsersByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (module *getter) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.User, error) {
|
||||
user, err := module.store.GetByOrgIDAndID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (module *getter) Get(ctx context.Context, id valuer.UUID) (*types.User, error) {
|
||||
storableUser, err := module.store.GetUser(ctx, id)
|
||||
user, err := module.store.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleNames, err := module.resolveRoleNamesForUser(ctx, storableUser.ID, storableUser.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := types.NewUserFromStorable(storableUser, roleNames)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (module *getter) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.User, error) {
|
||||
storableUsers, err := module.store.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs)
|
||||
users, err := module.store.ListUsersByEmailAndOrgIDs(ctx, email, orgIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*types.User, len(storableUsers))
|
||||
|
||||
for idx, storableUser := range storableUsers {
|
||||
roleNames, err := module.resolveRoleNamesForUser(ctx, storableUser.ID, storableUser.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users[idx] = types.NewUserFromStorable(storableUser, roleNames)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
@@ -135,52 +103,3 @@ func (module *getter) GetFactorPasswordByUserID(ctx context.Context, userID valu
|
||||
|
||||
return factorPassword, nil
|
||||
}
|
||||
|
||||
func (module *getter) resolveRoleNamesForUser(ctx context.Context, userID valuer.UUID, orgID valuer.UUID) ([]string, error) {
|
||||
storableUserRoles, err := module.userRoleStore.GetUserRolesByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleIDs := make([]valuer.UUID, len(storableUserRoles))
|
||||
for idx, sur := range storableUserRoles {
|
||||
roleIDs[idx] = sur.RoleID
|
||||
}
|
||||
|
||||
roles, err := module.authz.ListByOrgIDAndIDs(ctx, orgID, roleIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleNames := make([]string, len(roles))
|
||||
for idx, role := range roles {
|
||||
roleNames[idx] = role.Name
|
||||
}
|
||||
|
||||
return roleNames, nil
|
||||
}
|
||||
|
||||
func (module *getter) usersFromStorableUsersAndRolesMaps(storableUsers []*types.StorableUser, roles []*authtypes.Role, userIDToRoleIDsMap map[valuer.UUID][]valuer.UUID) []*types.User {
|
||||
users := make([]*types.User, 0, len(storableUsers))
|
||||
|
||||
roleIDToRole := make(map[string]*authtypes.Role, len(roles))
|
||||
for _, role := range roles {
|
||||
roleIDToRole[role.ID.String()] = role
|
||||
}
|
||||
|
||||
for _, user := range storableUsers {
|
||||
roleIDs := userIDToRoleIDsMap[user.ID]
|
||||
|
||||
roleNames := make([]string, 0, len(roleIDs))
|
||||
for _, rid := range roleIDs {
|
||||
if role, ok := roleIDToRole[rid.String()]; ok {
|
||||
roleNames = append(roleNames, role.Name)
|
||||
}
|
||||
}
|
||||
|
||||
account := types.NewUserFromStorable(user, roleNames)
|
||||
users = append(users, account)
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func (h *handler) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.module.GetByOrgIDAndUserID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(id))
|
||||
user, err := h.getter.GetByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(id))
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
@@ -188,7 +188,7 @@ func (h *handler) GetMyUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.module.GetByOrgIDAndUserID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID))
|
||||
user, err := h.getter.GetByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID))
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
@@ -270,7 +270,7 @@ func (handler *handler) GetResetPasswordToken(w http.ResponseWriter, r *http.Req
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
userID := mux.Vars(r)["id"]
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
@@ -278,7 +278,13 @@ func (handler *handler) GetResetPasswordToken(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
token, err := handler.module.GetOrCreateResetPasswordToken(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(userID))
|
||||
user, err := handler.getter.GetByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(id))
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := handler.module.GetOrCreateResetPasswordToken(ctx, user.ID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
root "github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/tokenizer"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
@@ -23,53 +24,34 @@ import (
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
store types.UserStore
|
||||
userRoleStore authtypes.UserRoleStore
|
||||
tokenizer tokenizer.Tokenizer
|
||||
emailing emailing.Emailing
|
||||
settings factory.ScopedProviderSettings
|
||||
orgSetter organization.Setter
|
||||
authz authz.AuthZ
|
||||
analytics analytics.Analytics
|
||||
config root.Config
|
||||
store types.UserStore
|
||||
tokenizer tokenizer.Tokenizer
|
||||
emailing emailing.Emailing
|
||||
settings factory.ScopedProviderSettings
|
||||
orgSetter organization.Setter
|
||||
authz authz.AuthZ
|
||||
analytics analytics.Analytics
|
||||
config user.Config
|
||||
}
|
||||
|
||||
// This module is a WIP, don't take inspiration from this.
|
||||
func NewModule(store types.UserStore, userRoleStore authtypes.UserRoleStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, authz authz.AuthZ, analytics analytics.Analytics, config root.Config) root.Module {
|
||||
func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, authz authz.AuthZ, analytics analytics.Analytics, config user.Config) root.Module {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser")
|
||||
return &Module{
|
||||
store: store,
|
||||
userRoleStore: userRoleStore,
|
||||
tokenizer: tokenizer,
|
||||
emailing: emailing,
|
||||
settings: settings,
|
||||
orgSetter: orgSetter,
|
||||
analytics: analytics,
|
||||
authz: authz,
|
||||
config: config,
|
||||
store: store,
|
||||
tokenizer: tokenizer,
|
||||
emailing: emailing,
|
||||
settings: settings,
|
||||
orgSetter: orgSetter,
|
||||
analytics: analytics,
|
||||
authz: authz,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// this function gets user with its proper roles populated
|
||||
func (m *Module) GetByOrgIDAndUserID(ctx context.Context, orgID, userID valuer.UUID) (*types.User, error) {
|
||||
storableUser, err := m.store.GetByOrgIDAndID(ctx, orgID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleNames, err := m.resolveRoleNamesForUser(ctx, userID, storableUser.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := types.NewUserFromStorable(storableUser, roleNames)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m *Module) AcceptInvite(ctx context.Context, token string, password string) (*types.User, error) {
|
||||
// get the user by reset password token
|
||||
storableUser, err := m.store.GetUserByResetPasswordToken(ctx, token)
|
||||
user, err := m.store.GetUserByResetPasswordToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -81,7 +63,7 @@ func (m *Module) AcceptInvite(ctx context.Context, token string, password string
|
||||
}
|
||||
|
||||
// query the user again
|
||||
user, err := m.GetByOrgIDAndUserID(ctx, storableUser.OrgID, storableUser.ID)
|
||||
user, err = m.store.GetByOrgIDAndID(ctx, user.OrgID, user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -91,12 +73,7 @@ func (m *Module) AcceptInvite(ctx context.Context, token string, password string
|
||||
|
||||
func (m *Module) GetInviteByToken(ctx context.Context, token string) (*types.Invite, error) {
|
||||
// get the user
|
||||
storableUser, err := m.store.GetUserByResetPasswordToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := m.GetByOrgIDAndUserID(ctx, storableUser.OrgID, storableUser.ID)
|
||||
user, err := m.store.GetUserByResetPasswordToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -110,7 +87,6 @@ func (m *Module) GetInviteByToken(ctx context.Context, token string) (*types.Inv
|
||||
Email: user.Email,
|
||||
Token: token,
|
||||
Role: user.Role,
|
||||
Roles: user.Roles,
|
||||
OrgID: user.OrgID,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: user.CreatedAt,
|
||||
@@ -130,52 +106,24 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
|
||||
|
||||
// validate all emails to be invited
|
||||
emails := make([]string, len(bulkInvites.Invites))
|
||||
var allRolesFromRequest []string
|
||||
seenRolesFromRequest := make(map[string]struct{})
|
||||
for idx := range bulkInvites.Invites {
|
||||
invite := &bulkInvites.Invites[idx]
|
||||
for idx, invite := range bulkInvites.Invites {
|
||||
emails[idx] = invite.Email.StringValue()
|
||||
|
||||
// backward compat: derive Roles from legacy Role when Roles is not provided
|
||||
if len(invite.Roles) == 0 && invite.Role != "" {
|
||||
if managedRole, ok := authtypes.ExistingRoleToSigNozManagedRoleMap[invite.Role]; ok {
|
||||
invite.Roles = []string{managedRole}
|
||||
}
|
||||
} else if invite.Role == "" && len(invite.Roles) > 0 {
|
||||
// and vice versa
|
||||
invite.Role = authtypes.HighestLegacyRoleFromManagedRoles(invite.Roles)
|
||||
}
|
||||
|
||||
// for role name validation
|
||||
for _, role := range invite.Roles {
|
||||
if _, ok := seenRolesFromRequest[role]; !ok {
|
||||
seenRolesFromRequest[role] = struct{}{}
|
||||
allRolesFromRequest = append(allRolesFromRequest, role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
storableUsers, err := m.store.GetUsersByEmailsOrgIDAndStatuses(ctx, orgID, emails, []string{types.UserStatusActive.StringValue(), types.UserStatusPendingInvite.StringValue()})
|
||||
users, err := m.store.GetUsersByEmailsOrgIDAndStatuses(ctx, orgID, emails, []string{types.UserStatusActive.StringValue(), types.UserStatusPendingInvite.StringValue()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(storableUsers) > 0 {
|
||||
if err := storableUsers[0].ErrIfRoot(); err != nil {
|
||||
if len(users) > 0 {
|
||||
if err := users[0].ErrIfRoot(); err != nil {
|
||||
return nil, errors.WithAdditionalf(err, "Cannot send invite to root user")
|
||||
}
|
||||
|
||||
if storableUsers[0].Status == types.UserStatusPendingInvite {
|
||||
return nil, errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "An invite already exists for this email: %s", storableUsers[0].Email.StringValue())
|
||||
if users[0].Status == types.UserStatusPendingInvite {
|
||||
return nil, errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "An invite already exists for this email: %s", users[0].Email.StringValue())
|
||||
}
|
||||
|
||||
return nil, errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "User already exists with this email: %s", storableUsers[0].Email.StringValue())
|
||||
}
|
||||
|
||||
// this function returns error if some role is not found by name
|
||||
_, err = m.authz.ListByOrgIDAndNames(ctx, orgID, allRolesFromRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Newf(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "User already exists with this email: %s", users[0].Email.StringValue())
|
||||
}
|
||||
|
||||
type userWithResetToken struct {
|
||||
@@ -187,20 +135,25 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
|
||||
|
||||
if err := m.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
for idx, invite := range bulkInvites.Invites {
|
||||
// create a new user with pending invite status
|
||||
newUser, err := types.NewUser(invite.Name, invite.Email, invite.Role, invite.Roles, orgID, types.UserStatusPendingInvite)
|
||||
role, err := types.NewRole(invite.Role.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// store the user and user_role entries in db
|
||||
// create a new user with pending invite status
|
||||
newUser, err := types.NewUser(invite.Name, invite.Email, role, orgID, types.UserStatusPendingInvite)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// store the user and password in db
|
||||
err = m.createUserWithoutGrant(ctx, newUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// generate reset password token
|
||||
resetPasswordToken, err := m.GetOrCreateResetPasswordToken(ctx, newUser.OrgID, newUser.ID)
|
||||
resetPasswordToken, err := m.GetOrCreateResetPasswordToken(ctx, newUser.ID)
|
||||
if err != nil {
|
||||
m.settings.Logger().ErrorContext(ctx, "failed to create reset password token for invited user", "error", err)
|
||||
return err
|
||||
@@ -222,7 +175,7 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
|
||||
for idx, userWithToken := range newUsersWithResetToken {
|
||||
m.analytics.TrackUser(ctx, orgID.String(), creator.ID.String(), "Invite Sent", map[string]any{
|
||||
"invitee_email": userWithToken.User.Email,
|
||||
"invitee_roles": userWithToken.User.Roles,
|
||||
"invitee_role": userWithToken.User.Role,
|
||||
})
|
||||
|
||||
invite := &types.Invite{
|
||||
@@ -233,7 +186,6 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
|
||||
Email: userWithToken.User.Email,
|
||||
Token: userWithToken.ResetPasswordToken.Token,
|
||||
Role: userWithToken.User.Role,
|
||||
Roles: userWithToken.User.Roles,
|
||||
OrgID: userWithToken.User.OrgID,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: userWithToken.User.CreatedAt,
|
||||
@@ -267,39 +219,39 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
|
||||
}
|
||||
|
||||
func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
|
||||
storableUsers, err := m.store.ListUsersByOrgID(ctx, valuer.MustNewUUID(orgID))
|
||||
// find all the users with pending_invite status
|
||||
users, err := m.store.ListUsersByOrgID(ctx, valuer.MustNewUUID(orgID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storableUsers = slices.DeleteFunc(storableUsers, func(user *types.StorableUser) bool { return user.Status != types.UserStatusPendingInvite })
|
||||
pendingUsers := slices.DeleteFunc(users, func(user *types.User) bool { return user.Status != types.UserStatusPendingInvite })
|
||||
|
||||
var invites []*types.Invite
|
||||
for _, su := range storableUsers {
|
||||
roleNames, err := m.resolveRoleNamesForUser(ctx, su.ID, su.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := types.NewUserFromStorable(su, roleNames)
|
||||
|
||||
resetPasswordToken, err := m.GetOrCreateResetPasswordToken(ctx, user.OrgID, user.ID)
|
||||
for _, pUser := range pendingUsers {
|
||||
// get the reset password token
|
||||
resetPasswordToken, err := m.GetOrCreateResetPasswordToken(ctx, pUser.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create a dummy invite obj for backward compatibility
|
||||
invite := &types.Invite{
|
||||
Identifiable: types.Identifiable{ID: user.ID},
|
||||
Name: user.DisplayName,
|
||||
Email: user.Email,
|
||||
Token: resetPasswordToken.Token,
|
||||
Role: user.Role,
|
||||
Roles: user.Roles,
|
||||
OrgID: user.OrgID,
|
||||
Identifiable: types.Identifiable{
|
||||
ID: pUser.ID,
|
||||
},
|
||||
Name: pUser.DisplayName,
|
||||
Email: pUser.Email,
|
||||
Token: resetPasswordToken.Token,
|
||||
Role: pUser.Role,
|
||||
OrgID: pUser.OrgID,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
CreatedAt: pUser.CreatedAt,
|
||||
UpdatedAt: pUser.UpdatedAt, // dummy
|
||||
},
|
||||
}
|
||||
|
||||
invites = append(invites, invite)
|
||||
}
|
||||
|
||||
@@ -307,27 +259,16 @@ func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.Invite,
|
||||
}
|
||||
|
||||
func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
|
||||
// validate the roles
|
||||
_, err := module.authz.ListByOrgIDAndNames(ctx, input.OrgID, input.Roles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// since assign is idempotant multiple calls to assign won't cause issues in case of retries, also we cannot run this in a transaction for now
|
||||
err = module.authz.Grant(ctx, input.OrgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.StringValue(), input.OrgID, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
createUserOpts := root.NewCreateUserOptions(opts...)
|
||||
|
||||
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.CreateUser(ctx, types.NewStorableUser(input)); err != nil {
|
||||
return err
|
||||
}
|
||||
// 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))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create user_role junction entries
|
||||
if err := module.createUserRoleEntries(ctx, input); err != nil {
|
||||
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.CreateUser(ctx, input); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -350,7 +291,7 @@ func (module *Module) CreateUser(ctx context.Context, input *types.User, opts ..
|
||||
}
|
||||
|
||||
func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.User, updatedBy string) (*types.User, error) {
|
||||
existingUser, err := m.GetByOrgIDAndUserID(ctx, orgID, valuer.MustNewUUID(id))
|
||||
existingUser, err := m.store.GetUser(ctx, valuer.MustNewUUID(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -367,30 +308,18 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
|
||||
return nil, errors.WithAdditionalf(err, "cannot update pending user")
|
||||
}
|
||||
|
||||
requestor, err := m.GetByOrgIDAndUserID(ctx, orgID, valuer.MustNewUUID(updatedBy))
|
||||
requestor, err := m.store.GetUser(ctx, valuer.MustNewUUID(updatedBy))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// backward compatibility: convert legacy "role" field to "roles" when "roles" is not provided
|
||||
if user.Roles == nil && user.Role != "" && user.Role != existingUser.Role {
|
||||
user.Roles = []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}
|
||||
}
|
||||
|
||||
var grants, revokes []string
|
||||
var rolesChanged bool
|
||||
if user.Roles != nil {
|
||||
grants, revokes = existingUser.PatchRoles(user.Roles)
|
||||
rolesChanged = (len(grants) > 0) || (len(revokes) > 0)
|
||||
}
|
||||
|
||||
if rolesChanged && !slices.Contains(requestor.Roles, authtypes.SigNozAdminRoleName) {
|
||||
if user.Role != "" && user.Role != existingUser.Role && requestor.Role != types.RoleAdmin {
|
||||
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "only admins can change roles")
|
||||
}
|
||||
|
||||
// Make sure that the request is not demoting the last admin user.
|
||||
if rolesChanged && slices.Contains(existingUser.Roles, authtypes.SigNozAdminRoleName) && !slices.Contains(user.Roles, authtypes.SigNozAdminRoleName) {
|
||||
adminUsers, err := m.store.GetActiveUsersByRoleNameAndOrgID(ctx, authtypes.SigNozAdminRoleName, orgID)
|
||||
if user.Role != "" && user.Role != existingUser.Role && existingUser.Role == types.RoleAdmin {
|
||||
adminUsers, err := m.store.GetActiveUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -400,58 +329,28 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
|
||||
}
|
||||
}
|
||||
|
||||
if rolesChanged {
|
||||
// can't run in txn
|
||||
err = m.authz.ModifyGrant(ctx, orgID, revokes, grants, authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil))
|
||||
if user.Role != "" && user.Role != existingUser.Role {
|
||||
err = m.authz.ModifyGrant(ctx,
|
||||
orgID,
|
||||
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role)},
|
||||
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
|
||||
authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// preserve existing role and roles when not explicitly provided in the request
|
||||
updateRole := user.Role
|
||||
updateRoles := user.Roles
|
||||
if user.Roles == nil {
|
||||
updateRole = existingUser.Role
|
||||
updateRoles = existingUser.Roles
|
||||
} else if updateRole == "" {
|
||||
updateRole = existingUser.Role
|
||||
}
|
||||
existingUser.Update(user.DisplayName, updateRole, updateRoles)
|
||||
if rolesChanged {
|
||||
err = m.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
// update the user
|
||||
if err := m.UpdateAnyUser(ctx, orgID, existingUser); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete old role entries and create new ones
|
||||
if err := m.userRoleStore.DeleteUserRoles(ctx, existingUser.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create new ones
|
||||
if err := m.createUserRoleEntries(ctx, existingUser); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// persist display name change even when roles haven't changed
|
||||
if err := m.UpdateAnyUser(ctx, orgID, existingUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
existingUser.Update(user.DisplayName, user.Role)
|
||||
if err := m.UpdateAnyUser(ctx, orgID, existingUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return existingUser, nil
|
||||
}
|
||||
|
||||
func (module *Module) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.User) error {
|
||||
storableUser := types.NewStorableUser(user)
|
||||
if err := module.store.UpdateUser(ctx, orgID, storableUser); err != nil {
|
||||
if err := module.store.UpdateUser(ctx, orgID, user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -467,7 +366,7 @@ func (module *Module) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user
|
||||
}
|
||||
|
||||
func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error {
|
||||
user, err := module.GetByOrgIDAndUserID(ctx, orgID, valuer.MustNewUUID(id))
|
||||
user, err := module.store.GetUser(ctx, valuer.MustNewUUID(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -485,17 +384,17 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
|
||||
}
|
||||
|
||||
// don't allow to delete the last admin user
|
||||
adminUsers, err := module.store.GetActiveUsersByRoleNameAndOrgID(ctx, authtypes.SigNozAdminRoleName, orgID)
|
||||
adminUsers, err := module.store.GetActiveUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(adminUsers) == 1 && slices.Contains(user.Roles, authtypes.SigNozAdminRoleName) {
|
||||
if len(adminUsers) == 1 && user.Role == types.RoleAdmin {
|
||||
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot delete the last admin")
|
||||
}
|
||||
|
||||
// since revoke is idempotant multiple calls to revoke won't cause issues in case of retries
|
||||
err = module.authz.Revoke(ctx, orgID, user.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil))
|
||||
err = module.authz.Revoke(ctx, orgID, []string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)}, authtypes.MustNewSubject(authtypes.TypeableUser, id, orgID, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -512,8 +411,8 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, orgID, userID valuer.UUID) (*types.ResetPasswordToken, error) {
|
||||
user, err := module.GetByOrgIDAndUserID(ctx, orgID, userID)
|
||||
func (module *Module) GetOrCreateResetPasswordToken(ctx context.Context, userID valuer.UUID) (*types.ResetPasswordToken, error) {
|
||||
user, err := module.store.GetUser(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -596,7 +495,7 @@ func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, ema
|
||||
return errors.WithAdditionalf(err, "cannot reset password for root user")
|
||||
}
|
||||
|
||||
token, err := module.GetOrCreateResetPasswordToken(ctx, orgID, user.ID)
|
||||
token, err := module.GetOrCreateResetPasswordToken(ctx, user.ID)
|
||||
if err != nil {
|
||||
module.settings.Logger().ErrorContext(ctx, "failed to create reset password token", "error", err)
|
||||
return err
|
||||
@@ -642,17 +541,17 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
|
||||
return err
|
||||
}
|
||||
|
||||
storableUser, err := module.store.GetUser(ctx, valuer.MustNewUUID(password.UserID))
|
||||
user, err := module.store.GetUser(ctx, valuer.MustNewUUID(password.UserID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// handle deleted user
|
||||
if err := storableUser.ErrIfDeleted(); err != nil {
|
||||
if err := user.ErrIfDeleted(); err != nil {
|
||||
return errors.WithAdditionalf(err, "deleted users cannot reset their password")
|
||||
}
|
||||
|
||||
if err := storableUser.ErrIfRoot(); err != nil {
|
||||
if err := user.ErrIfRoot(); err != nil {
|
||||
return errors.WithAdditionalf(err, "cannot reset password for root user")
|
||||
}
|
||||
|
||||
@@ -660,19 +559,12 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
|
||||
return err
|
||||
}
|
||||
|
||||
roleNames, err := module.resolveRoleNamesForUser(ctx, storableUser.ID, storableUser.OrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := types.NewUserFromStorable(storableUser, roleNames)
|
||||
|
||||
// since grant is idempotent, multiple calls won't cause issues in case of retries
|
||||
if user.Status == types.UserStatusPendingInvite {
|
||||
if err = module.authz.Grant(
|
||||
ctx,
|
||||
user.OrgID,
|
||||
user.Roles,
|
||||
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
|
||||
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
|
||||
); err != nil {
|
||||
return err
|
||||
@@ -684,7 +576,7 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
|
||||
if err := user.UpdateStatus(types.UserStatusActive); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := module.store.UpdateUser(ctx, user.OrgID, types.NewStorableUser(user)); err != nil {
|
||||
if err := module.store.UpdateUser(ctx, user.OrgID, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -702,16 +594,16 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
|
||||
}
|
||||
|
||||
func (module *Module) UpdatePassword(ctx context.Context, userID valuer.UUID, oldpasswd string, passwd string) error {
|
||||
storableUser, err := module.store.GetUser(ctx, userID)
|
||||
user, err := module.store.GetUser(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := storableUser.ErrIfDeleted(); err != nil {
|
||||
if err := user.ErrIfDeleted(); err != nil {
|
||||
return errors.WithAdditionalf(err, "cannot change password for deleted user")
|
||||
}
|
||||
|
||||
if err := storableUser.ErrIfRoot(); err != nil {
|
||||
if err := user.ErrIfRoot(); err != nil {
|
||||
return errors.WithAdditionalf(err, "cannot change password for root user")
|
||||
}
|
||||
|
||||
@@ -756,12 +648,10 @@ func (module *Module) GetOrCreateUser(ctx context.Context, user *types.User, opt
|
||||
if existingUser != nil {
|
||||
// for users logging through SSO flow but are having status as pending_invite
|
||||
if existingUser.Status == types.UserStatusPendingInvite {
|
||||
// capture old roles before overwriting with SSO roles
|
||||
oldRoles := existingUser.Roles
|
||||
// respect the role coming from the SSO
|
||||
existingUser.Update("", user.Role, user.Roles)
|
||||
existingUser.Update("", user.Role)
|
||||
// activate the user
|
||||
if err = module.activatePendingUser(ctx, existingUser, oldRoles); err != nil {
|
||||
if err = module.activatePendingUser(ctx, existingUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -798,7 +688,7 @@ func (m *Module) RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UU
|
||||
}
|
||||
|
||||
func (module *Module) CreateFirstUser(ctx context.Context, organization *types.Organization, name string, email valuer.Email, passwd string) (*types.User, error) {
|
||||
user, err := types.NewRootUser(name, email, organization.ID, []string{authtypes.SigNozAdminRoleName})
|
||||
user, err := types.NewRootUser(name, email, organization.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -860,24 +750,20 @@ func (module *Module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
|
||||
|
||||
// this function restricts that only one non-deleted user email can exist for an org ID, if found more, it throws an error
|
||||
func (module *Module) GetNonDeletedUserByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) (*types.User, error) {
|
||||
existingStorableUsers, err := module.store.GetUsersByEmailAndOrgID(ctx, email, orgID)
|
||||
existingUsers, err := module.store.GetUsersByEmailAndOrgID(ctx, email, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// filter out the deleted users
|
||||
existingStorableUsers = slices.DeleteFunc(existingStorableUsers, func(user *types.StorableUser) bool { return user.ErrIfDeleted() != nil })
|
||||
existingUsers = slices.DeleteFunc(existingUsers, func(user *types.User) bool { return user.ErrIfDeleted() != nil })
|
||||
|
||||
if len(existingStorableUsers) > 1 {
|
||||
if len(existingUsers) > 1 {
|
||||
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "Multiple non-deleted users found for email %s in org_id: %s", email.StringValue(), orgID.StringValue())
|
||||
}
|
||||
|
||||
if len(existingStorableUsers) == 1 {
|
||||
existingUser, err := module.GetByOrgIDAndUserID(ctx, existingStorableUsers[0].OrgID, existingStorableUsers[0].ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return existingUser, nil
|
||||
if len(existingUsers) == 1 {
|
||||
return existingUsers[0], nil
|
||||
}
|
||||
|
||||
return nil, errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "No non-deleted user found with email %s in org_id: %s", email.StringValue(), orgID.StringValue())
|
||||
@@ -887,12 +773,7 @@ func (module *Module) GetNonDeletedUserByEmailAndOrgID(ctx context.Context, emai
|
||||
func (module *Module) createUserWithoutGrant(ctx context.Context, input *types.User, opts ...root.CreateUserOption) error {
|
||||
createUserOpts := root.NewCreateUserOptions(opts...)
|
||||
if err := module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.CreateUser(ctx, types.NewStorableUser(input)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create user_role junction entries
|
||||
if err := module.createUserRoleEntries(ctx, input); err != nil {
|
||||
if err := module.store.CreateUser(ctx, input); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -914,27 +795,11 @@ func (module *Module) createUserWithoutGrant(ctx context.Context, input *types.U
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *Module) createUserRoleEntries(ctx context.Context, user *types.User) error {
|
||||
if len(user.Roles) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
storableRoles, err := module.authz.ListByOrgIDAndNames(ctx, user.OrgID, user.Roles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userRoles := authtypes.NewStorableUserRoles(user.ID, storableRoles)
|
||||
return module.userRoleStore.CreateUserRoles(ctx, userRoles)
|
||||
}
|
||||
|
||||
func (module *Module) activatePendingUser(ctx context.Context, user *types.User, oldRoles []string) error {
|
||||
// use ModifyGrant to revoke old invite roles and grant new SSO roles
|
||||
err := module.authz.ModifyGrant(
|
||||
func (module *Module) activatePendingUser(ctx context.Context, user *types.User) error {
|
||||
err := module.authz.Grant(
|
||||
ctx,
|
||||
user.OrgID,
|
||||
oldRoles,
|
||||
user.Roles,
|
||||
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(user.Role)},
|
||||
authtypes.MustNewSubject(authtypes.TypeableUser, user.ID.StringValue(), user.OrgID, nil),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -944,66 +809,10 @@ func (module *Module) activatePendingUser(ctx context.Context, user *types.User,
|
||||
if err := user.UpdateStatus(types.UserStatusActive); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := module.store.UpdateUser(ctx, user.OrgID, types.NewStorableUser(user)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete old invite role entries and create new ones from SSO
|
||||
if err := module.userRoleStore.DeleteUserRoles(ctx, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return module.createUserRoleEntries(ctx, user)
|
||||
})
|
||||
}
|
||||
|
||||
func (module *Module) usersFromStorableUsersAndRolesMaps(storableUsers []*types.StorableUser, roles []*authtypes.Role, userIDToRoleIDsMap map[valuer.UUID][]valuer.UUID) []*types.User {
|
||||
users := make([]*types.User, 0, len(storableUsers))
|
||||
|
||||
roleIDToRole := make(map[string]*authtypes.Role, len(roles))
|
||||
for _, role := range roles {
|
||||
roleIDToRole[role.ID.String()] = role
|
||||
}
|
||||
|
||||
for _, user := range storableUsers {
|
||||
roleIDs := userIDToRoleIDsMap[user.ID]
|
||||
|
||||
roleNames := make([]string, 0, len(roleIDs))
|
||||
for _, rid := range roleIDs {
|
||||
if role, ok := roleIDToRole[rid.String()]; ok {
|
||||
roleNames = append(roleNames, role.Name)
|
||||
}
|
||||
}
|
||||
|
||||
account := types.NewUserFromStorable(user, roleNames)
|
||||
users = append(users, account)
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
func (m *Module) resolveRoleNamesForUser(ctx context.Context, userID valuer.UUID, orgID valuer.UUID) ([]string, error) {
|
||||
storableUserRoles, err := m.userRoleStore.GetUserRolesByUserID(ctx, userID)
|
||||
err = module.store.UpdateUser(ctx, user.OrgID, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
roleIDs := make([]valuer.UUID, len(storableUserRoles))
|
||||
for idx, sur := range storableUserRoles {
|
||||
roleIDs[idx] = sur.RoleID
|
||||
}
|
||||
|
||||
roles, err := m.authz.ListByOrgIDAndIDs(ctx, orgID, roleIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleNames := make([]string, len(roles))
|
||||
for idx, role := range roles {
|
||||
roleNames[idx] = role.Name
|
||||
}
|
||||
|
||||
return roleNames, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,34 +15,31 @@ import (
|
||||
)
|
||||
|
||||
type service struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
store types.UserStore
|
||||
userRoleStore authtypes.UserRoleStore
|
||||
module user.Module
|
||||
orgGetter organization.Getter
|
||||
authz authz.AuthZ
|
||||
config user.RootConfig
|
||||
stopC chan struct{}
|
||||
settings factory.ScopedProviderSettings
|
||||
store types.UserStore
|
||||
module user.Module
|
||||
orgGetter organization.Getter
|
||||
authz authz.AuthZ
|
||||
config user.RootConfig
|
||||
stopC chan struct{}
|
||||
}
|
||||
|
||||
func NewService(
|
||||
providerSettings factory.ProviderSettings,
|
||||
store types.UserStore,
|
||||
userRoleStore authtypes.UserRoleStore,
|
||||
module user.Module,
|
||||
orgGetter organization.Getter,
|
||||
authz authz.AuthZ,
|
||||
config user.RootConfig,
|
||||
) user.Service {
|
||||
return &service{
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/pkg/modules/user"),
|
||||
store: store,
|
||||
userRoleStore: userRoleStore,
|
||||
module: module,
|
||||
orgGetter: orgGetter,
|
||||
authz: authz,
|
||||
config: config,
|
||||
stopC: make(chan struct{}),
|
||||
settings: factory.NewScopedProviderSettings(providerSettings, "go.signoz.io/pkg/modules/user"),
|
||||
store: store,
|
||||
module: module,
|
||||
orgGetter: orgGetter,
|
||||
authz: authz,
|
||||
config: config,
|
||||
stopC: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,33 +77,59 @@ func (s *service) Stop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (s *service) reconcile(ctx context.Context) error {
|
||||
org, resolvedByName, err := s.orgGetter.GetByIDOrName(ctx, s.config.Org.ID, s.config.Org.Name)
|
||||
if !s.config.Org.ID.IsZero() {
|
||||
return s.reconcileWithOrgID(ctx)
|
||||
}
|
||||
|
||||
return s.reconcileByName(ctx)
|
||||
}
|
||||
|
||||
func (s *service) reconcileWithOrgID(ctx context.Context) error {
|
||||
org, err := s.orgGetter.Get(ctx, s.config.Org.ID)
|
||||
if err != nil {
|
||||
if !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err // something really went wrong
|
||||
}
|
||||
|
||||
if s.config.Org.ID.IsZero() {
|
||||
newOrg := types.NewOrganization(s.config.Org.Name, s.config.Org.Name)
|
||||
_, err := s.module.CreateFirstUser(ctx, newOrg, s.config.Email.String(), s.config.Email, s.config.Password)
|
||||
return err
|
||||
// org was not found using id check if we can find an org using name
|
||||
|
||||
existingOrgByName, nameErr := s.orgGetter.GetByName(ctx, s.config.Org.Name)
|
||||
if nameErr != nil && !errors.Ast(nameErr, errors.TypeNotFound) {
|
||||
return nameErr // something really went wrong
|
||||
}
|
||||
|
||||
// we found an org using name
|
||||
if existingOrgByName != nil {
|
||||
// the existing org has the same name as config but org id is different inform user with actionable message
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "organization with name %q already exists with a different ID %s (expected %s)", s.config.Org.Name, existingOrgByName.ID.StringValue(), s.config.Org.ID.StringValue())
|
||||
}
|
||||
|
||||
// default - we did not found any org using id and name both - create a new org
|
||||
newOrg := types.NewOrganizationWithID(s.config.Org.ID, s.config.Org.Name, s.config.Org.Name)
|
||||
_, err = s.module.CreateFirstUser(ctx, newOrg, s.config.Email.String(), s.config.Email, s.config.Password)
|
||||
return err
|
||||
}
|
||||
|
||||
if !s.config.Org.ID.IsZero() && resolvedByName {
|
||||
// the existing org has the same name as config but org id is different; inform user with actionable message
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "organization with name %q already exists with a different ID %s (expected %s)", s.config.Org.Name, org.ID.StringValue(), s.config.Org.ID.StringValue())
|
||||
return s.reconcileRootUser(ctx, org.ID)
|
||||
}
|
||||
|
||||
func (s *service) reconcileByName(ctx context.Context) error {
|
||||
org, err := s.orgGetter.GetByName(ctx, s.config.Org.Name)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
newOrg := types.NewOrganization(s.config.Org.Name, s.config.Org.Name)
|
||||
_, err := s.module.CreateFirstUser(ctx, newOrg, s.config.Email.String(), s.config.Email, s.config.Password)
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return s.reconcileRootUser(ctx, org.ID)
|
||||
}
|
||||
|
||||
func (s *service) reconcileRootUser(ctx context.Context, orgID valuer.UUID) error {
|
||||
existingRoot, err := s.getRootUserByOrgID(ctx, orgID)
|
||||
existingRoot, err := s.store.GetRootUserByOrgID(ctx, orgID)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
@@ -125,49 +148,29 @@ func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID
|
||||
}
|
||||
|
||||
if existingUser != nil {
|
||||
oldRoles := existingUser.Roles
|
||||
oldRole := existingUser.Role
|
||||
|
||||
existingUser.PromoteToRoot() // this only sets the column is_root as true (permissions are managed by authz in next step)
|
||||
existingUser.Roles = []string{authtypes.SigNozAdminRoleName}
|
||||
|
||||
// authz grant is idempotent and safe to retry, so do it before DB mutations
|
||||
if err := s.authz.ModifyGrant(ctx,
|
||||
orgID,
|
||||
oldRoles,
|
||||
[]string{authtypes.SigNozAdminRoleName},
|
||||
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), orgID, nil),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// this is idempotent
|
||||
existingUser.PromoteToRoot()
|
||||
if err := s.module.UpdateAnyUser(ctx, orgID, existingUser); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// resolve the admin role ID for user_role entries
|
||||
storableRoles, err := s.authz.ListByOrgIDAndNames(ctx, orgID, []string{authtypes.SigNozAdminRoleName})
|
||||
if err != nil {
|
||||
return err
|
||||
if oldRole != types.RoleAdmin {
|
||||
if err := s.authz.ModifyGrant(ctx,
|
||||
orgID,
|
||||
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(oldRole)},
|
||||
[]string{authtypes.MustGetSigNozManagedRoleFromExistingRole(types.RoleAdmin)},
|
||||
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), orgID, nil),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// wrap user_role updates and password in a transaction
|
||||
return s.store.RunInTx(ctx, func(ctx context.Context) error {
|
||||
if err := s.userRoleStore.DeleteUserRoles(ctx, existingUser.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userRoles := authtypes.NewStorableUserRoles(existingUser.ID, storableRoles)
|
||||
if err := s.userRoleStore.CreateUserRoles(ctx, userRoles); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.setPassword(ctx, existingUser.ID)
|
||||
})
|
||||
return s.setPassword(ctx, existingUser.ID)
|
||||
}
|
||||
|
||||
// Create new root user
|
||||
newUser, err := types.NewRootUser(s.config.Email.String(), s.config.Email, orgID, []string{authtypes.SigNozAdminRoleName})
|
||||
newUser, err := types.NewRootUser(s.config.Email.String(), s.config.Email, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -177,7 +180,6 @@ func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID
|
||||
return err
|
||||
}
|
||||
|
||||
// authz grants are handled inside CreateUser
|
||||
return s.module.CreateUser(ctx, newUser, user.WithFactorPassword(factorPassword))
|
||||
}
|
||||
|
||||
@@ -219,12 +221,3 @@ func (s *service) setPassword(ctx context.Context, userID valuer.UUID) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) getRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.User, error) {
|
||||
storableRoot, err := s.store.GetRootUserByOrgID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.module.GetByOrgIDAndUserID(ctx, orgID, storableRoot.ID)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func (store *store) CreatePassword(ctx context.Context, password *types.FactorPa
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) CreateUser(ctx context.Context, user *types.StorableUser) error {
|
||||
func (store *store) CreateUser(ctx context.Context, user *types.User) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -52,8 +52,8 @@ func (store *store) CreateUser(ctx context.Context, user *types.StorableUser) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*types.StorableUser, error) {
|
||||
var users []*types.StorableUser
|
||||
func (store *store) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]*types.User, error) {
|
||||
var users []*types.User
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -69,8 +69,8 @@ func (store *store) GetUsersByEmail(ctx context.Context, email valuer.Email) ([]
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (store *store) GetUser(ctx context.Context, id valuer.UUID) (*types.StorableUser, error) {
|
||||
user := new(types.StorableUser)
|
||||
func (store *store) GetUser(ctx context.Context, id valuer.UUID) (*types.User, error) {
|
||||
user := new(types.User)
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -86,8 +86,8 @@ func (store *store) GetUser(ctx context.Context, id valuer.UUID) (*types.Storabl
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (store *store) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.StorableUser, error) {
|
||||
user := new(types.StorableUser)
|
||||
func (store *store) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.User, error) {
|
||||
user := new(types.User)
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -104,8 +104,8 @@ func (store *store) GetByOrgIDAndID(ctx context.Context, orgID valuer.UUID, id v
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (store *store) GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*types.StorableUser, error) {
|
||||
var users []*types.StorableUser
|
||||
func (store *store) GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Email, orgID valuer.UUID) ([]*types.User, error) {
|
||||
var users []*types.User
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -122,7 +122,26 @@ func (store *store) GetUsersByEmailAndOrgID(ctx context.Context, email valuer.Em
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *types.StorableUser) error {
|
||||
func (store *store) GetActiveUsersByRoleAndOrgID(ctx context.Context, role types.Role, orgID valuer.UUID) ([]*types.User, error) {
|
||||
var users []*types.User
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(&users).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("role = ?", role).
|
||||
Where("status = ?", types.UserStatusActive.StringValue()).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *types.User) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -143,8 +162,8 @@ func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *typ
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *store) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.StorableUser, error) {
|
||||
users := []*types.StorableUser{}
|
||||
func (store *store) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.GettableUser, error) {
|
||||
users := []*types.User{}
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -228,7 +247,7 @@ func (store *store) DeleteUser(ctx context.Context, orgID string, id string) err
|
||||
|
||||
// delete user
|
||||
_, err = tx.NewDelete().
|
||||
Model(new(types.StorableUser)).
|
||||
Model(new(types.User)).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("id = ?", id).
|
||||
Exec(ctx)
|
||||
@@ -313,7 +332,7 @@ func (store *store) SoftDeleteUser(ctx context.Context, orgID string, id string)
|
||||
// soft delete user
|
||||
now := time.Now()
|
||||
_, err = tx.NewUpdate().
|
||||
Model(new(types.StorableUser)).
|
||||
Model(new(types.User)).
|
||||
Set("status = ?", types.UserStatusDeleted).
|
||||
Set("deleted_at = ?", now).
|
||||
Set("updated_at = ?", now).
|
||||
@@ -544,7 +563,7 @@ func (store *store) GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*type
|
||||
}
|
||||
|
||||
func (store *store) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) {
|
||||
user := new(types.StorableUser)
|
||||
user := new(types.User)
|
||||
|
||||
count, err := store.
|
||||
sqlstore.
|
||||
@@ -561,7 +580,7 @@ func (store *store) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64,
|
||||
}
|
||||
|
||||
func (store *store) CountByOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, statuses []string) (map[valuer.String]int64, error) {
|
||||
user := new(types.StorableUser)
|
||||
user := new(types.User)
|
||||
var results []struct {
|
||||
Status valuer.String `bun:"status"`
|
||||
Count int64 `bun:"count"`
|
||||
@@ -614,8 +633,8 @@ func (store *store) RunInTx(ctx context.Context, cb func(ctx context.Context) er
|
||||
})
|
||||
}
|
||||
|
||||
func (store *store) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.StorableUser, error) {
|
||||
user := new(types.StorableUser)
|
||||
func (store *store) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.User, error) {
|
||||
user := new(types.User)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
@@ -630,8 +649,8 @@ func (store *store) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (store *store) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.StorableUser, error) {
|
||||
users := []*types.StorableUser{}
|
||||
func (store *store) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.User, error) {
|
||||
users := []*types.User{}
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
@@ -647,15 +666,15 @@ func (store *store) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (store *store) GetUserByResetPasswordToken(ctx context.Context, token string) (*types.StorableUser, error) {
|
||||
user := new(types.StorableUser)
|
||||
func (store *store) GetUserByResetPasswordToken(ctx context.Context, token string) (*types.User, error) {
|
||||
user := new(types.User)
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(user).
|
||||
Join(`JOIN factor_password ON factor_password.user_id = "users".id`).
|
||||
Join(`JOIN factor_password ON factor_password.user_id = "user".id`).
|
||||
Join("JOIN reset_password_token ON reset_password_token.password_id = factor_password.id").
|
||||
Where("reset_password_token.token = ?", token).
|
||||
Scan(ctx)
|
||||
@@ -666,8 +685,8 @@ func (store *store) GetUserByResetPasswordToken(ctx context.Context, token strin
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (store *store) GetUsersByEmailsOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, emails []string, statuses []string) ([]*types.StorableUser, error) {
|
||||
users := []*types.StorableUser{}
|
||||
func (store *store) GetUsersByEmailsOrgIDAndStatuses(ctx context.Context, orgID valuer.UUID, emails []string, statuses []string) ([]*types.User, error) {
|
||||
users := []*types.User{}
|
||||
|
||||
err := store.
|
||||
sqlstore.
|
||||
@@ -684,20 +703,3 @@ func (store *store) GetUsersByEmailsOrgIDAndStatuses(ctx context.Context, orgID
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (store *store) GetActiveUsersByRoleNameAndOrgID(ctx context.Context, roleName string, orgID valuer.UUID) ([]*types.StorableUser, error) {
|
||||
var users []*types.StorableUser
|
||||
|
||||
err := store.sqlstore.BunDBCtx(ctx).NewSelect().
|
||||
Model(&users).
|
||||
Join("JOIN user_role ON user_role.user_id = users.id").
|
||||
Join("JOIN role ON role.id = user_role.role_id").
|
||||
Where("users.org_id = ?", orgID).
|
||||
Where("role.name = ?", roleName).
|
||||
Where("users.status = ?", types.UserStatusActive.StringValue()).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package impluser
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type userRoleStore struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
settings factory.ProviderSettings
|
||||
}
|
||||
|
||||
func NewUserRoleStore(sqlstore sqlstore.SQLStore, settings factory.ProviderSettings) authtypes.UserRoleStore {
|
||||
return &userRoleStore{sqlstore: sqlstore, settings: settings}
|
||||
}
|
||||
|
||||
func (store *userRoleStore) ListUserRolesByOrgIDAndUserIDs(ctx context.Context, orgID valuer.UUID, userIDs []valuer.UUID) ([]*authtypes.StorableUserRole, error) {
|
||||
storableUserRoles := make([]*authtypes.StorableUserRole, 0)
|
||||
|
||||
err := store.sqlstore.BunDBCtx(ctx).NewSelect().Model(&storableUserRoles).
|
||||
Join("JOIN users").
|
||||
JoinOn("users.id = user_role.user_id").
|
||||
Where("users.org_id = ?", orgID).Where("users.id IN (?)", bun.In(userIDs)).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storableUserRoles, nil
|
||||
}
|
||||
|
||||
func (store *userRoleStore) CreateUserRoles(ctx context.Context, userRoles []*authtypes.StorableUserRole) error {
|
||||
_, err := store.sqlstore.BunDBCtx(ctx).NewInsert().Model(&userRoles).Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapAlreadyExistsErrf(err, authtypes.ErrCodeUserRoleAlreadyExists, "duplicate role assignments for service account")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *userRoleStore) DeleteUserRoles(ctx context.Context, userID valuer.UUID) error {
|
||||
_, err := store.sqlstore.BunDBCtx(ctx).NewDelete().Model(new(authtypes.StorableUserRole)).Where("user_id = ?", userID).Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *userRoleStore) GetUserRolesByUserID(ctx context.Context, userID valuer.UUID) ([]*authtypes.StorableUserRole, error) {
|
||||
storableUserRoles := make([]*authtypes.StorableUserRole, 0)
|
||||
|
||||
err := store.sqlstore.BunDBCtx(ctx).NewSelect().Model(&storableUserRoles).Where("user_id = ?", userID).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storableUserRoles, nil
|
||||
}
|
||||
@@ -10,9 +10,6 @@ import (
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
// Gets user by org id and user id, this includes the roles resolution
|
||||
GetByOrgIDAndUserID(ctx context.Context, orgID, userID valuer.UUID) (*types.User, error)
|
||||
|
||||
// Creates the organization and the first user of that organization.
|
||||
CreateFirstUser(ctx context.Context, organization *types.Organization, name string, email valuer.Email, password string) (*types.User, error)
|
||||
|
||||
@@ -24,7 +21,7 @@ type Module interface {
|
||||
|
||||
// Get or Create a reset password token for a user. If the password does not exist, a new one is randomly generated and inserted. The function
|
||||
// is idempotent and can be called multiple times.
|
||||
GetOrCreateResetPasswordToken(ctx context.Context, orgID, userID valuer.UUID) (*types.ResetPasswordToken, error)
|
||||
GetOrCreateResetPasswordToken(ctx context.Context, userID valuer.UUID) (*types.ResetPasswordToken, error)
|
||||
|
||||
// Updates password of a user using a reset password token. It also deletes all reset password tokens for the user.
|
||||
// This is used to reset the password of a user when they forget their password.
|
||||
@@ -67,6 +64,12 @@ type Getter interface {
|
||||
// Get gets the users based on the given id
|
||||
ListByOrgID(context.Context, valuer.UUID) ([]*types.User, error)
|
||||
|
||||
// Get users by email.
|
||||
GetUsersByEmail(context.Context, valuer.Email) ([]*types.User, error)
|
||||
|
||||
// Get user by orgID and id.
|
||||
GetByOrgIDAndID(context.Context, valuer.UUID, valuer.UUID) (*types.User, error)
|
||||
|
||||
// Get user by id.
|
||||
Get(context.Context, valuer.UUID) (*types.User, error)
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ import (
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type builderQuery[T any] struct {
|
||||
@@ -260,6 +262,40 @@ func (q *builderQuery[T]) executeWithContext(ctx context.Context, query string,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// merge body_json and promoted into body
|
||||
if q.spec.Signal == telemetrytypes.SignalLogs {
|
||||
switch typedPayload := payload.(type) {
|
||||
case *qbtypes.RawData:
|
||||
for _, rr := range typedPayload.Rows {
|
||||
seeder := func() error {
|
||||
body, ok := rr.Data[telemetrylogs.LogsV2BodyJSONColumn].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
promoted, ok := rr.Data[telemetrylogs.LogsV2BodyPromotedColumn].(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
seed(promoted, body)
|
||||
str, err := sonic.MarshalString(body)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to marshal body")
|
||||
}
|
||||
rr.Data["body"] = str
|
||||
return nil
|
||||
}
|
||||
err := seeder()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
delete(rr.Data, telemetrylogs.LogsV2BodyJSONColumn)
|
||||
delete(rr.Data, telemetrylogs.LogsV2BodyPromotedColumn)
|
||||
}
|
||||
payload = typedPayload
|
||||
}
|
||||
}
|
||||
|
||||
return &qbtypes.Result{
|
||||
Type: q.kind,
|
||||
Value: payload,
|
||||
@@ -387,3 +423,18 @@ func decodeCursor(cur string) (int64, error) {
|
||||
}
|
||||
return strconv.ParseInt(string(b), 10, 64)
|
||||
}
|
||||
|
||||
func seed(promoted map[string]any, body map[string]any) {
|
||||
for key, fromValue := range promoted {
|
||||
if toValue, ok := body[key]; !ok {
|
||||
body[key] = fromValue
|
||||
} else {
|
||||
if fromValue, ok := fromValue.(map[string]any); ok {
|
||||
if toValue, ok := toValue.(map[string]any); ok {
|
||||
seed(fromValue, toValue)
|
||||
body[key] = toValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -393,11 +394,17 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
|
||||
|
||||
// de-reference the typed pointer to any
|
||||
val := reflect.ValueOf(cellPtr).Elem().Interface()
|
||||
// Post-process JSON columns: normalize into String value
|
||||
|
||||
// Post-process JSON columns: normalize into structured values
|
||||
if strings.HasPrefix(strings.ToUpper(colTypes[i].DatabaseTypeName()), "JSON") {
|
||||
switch x := val.(type) {
|
||||
case []byte:
|
||||
val = string(x)
|
||||
if len(x) > 0 {
|
||||
var v any
|
||||
if err := sonic.Unmarshal(x, &v); err == nil {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
default:
|
||||
// already a structured type (map[string]any, []any, etc.)
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func (r *cloudProviderAccountsSQLRepository) upsert(
|
||||
onConflictClause := ""
|
||||
if len(onConflictSetStmts) > 0 {
|
||||
onConflictClause = fmt.Sprintf(
|
||||
"conflict(id) do update SET\n%s",
|
||||
"conflict(id, provider, org_id) do update SET\n%s",
|
||||
strings.Join(onConflictSetStmts, ",\n"),
|
||||
)
|
||||
}
|
||||
@@ -202,8 +202,6 @@ func (r *cloudProviderAccountsSQLRepository) upsert(
|
||||
Exec(ctx)
|
||||
|
||||
if dbErr != nil {
|
||||
// for now returning internal error even if there is a conflict,
|
||||
// will be handled better in the future iteration
|
||||
return nil, model.InternalError(fmt.Errorf(
|
||||
"could not upsert cloud account record: %w", dbErr,
|
||||
))
|
||||
|
||||
@@ -131,38 +131,32 @@ func (ic *LogParsingPipelineController) ValidatePipelines(ctx context.Context,
|
||||
return err
|
||||
}
|
||||
|
||||
func (ic *LogParsingPipelineController) getDefaultPipelines() ([]pipelinetypes.GettablePipeline, error) {
|
||||
defaultPipelines := []pipelinetypes.GettablePipeline{}
|
||||
if querybuilder.BodyJSONQueryEnabled {
|
||||
preprocessingPipeline := pipelinetypes.GettablePipeline{
|
||||
StoreablePipeline: pipelinetypes.StoreablePipeline{
|
||||
Name: "Default Pipeline - PreProcessing Body",
|
||||
Alias: "NormalizeBodyDefault",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
Key: v3.AttributeKey{
|
||||
Key: "body",
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
},
|
||||
},
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
func (ic *LogParsingPipelineController) getNormalizePipeline() pipelinetypes.GettablePipeline {
|
||||
return pipelinetypes.GettablePipeline{
|
||||
StoreablePipeline: pipelinetypes.StoreablePipeline{
|
||||
Name: "Default Pipeline - PreProcessing Body",
|
||||
Alias: "NormalizeBodyDefault",
|
||||
Enabled: true,
|
||||
},
|
||||
Filter: &v3.FilterSet{
|
||||
Items: []v3.FilterItem{
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
Type: "normalize",
|
||||
Enabled: true,
|
||||
If: "body != nil",
|
||||
Key: v3.AttributeKey{
|
||||
Key: "body",
|
||||
},
|
||||
Operator: v3.FilterOperatorExists,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
defaultPipelines = append(defaultPipelines, preprocessingPipeline)
|
||||
},
|
||||
Config: []pipelinetypes.PipelineOperator{
|
||||
{
|
||||
ID: uuid.NewString(),
|
||||
Type: "normalize",
|
||||
Enabled: true,
|
||||
If: "body != nil",
|
||||
},
|
||||
},
|
||||
}
|
||||
return defaultPipelines, nil
|
||||
}
|
||||
|
||||
// Returns effective list of pipelines including user created
|
||||
@@ -295,12 +289,10 @@ func (pc *LogParsingPipelineController) RecommendAgentConfig(
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// recommend default pipelines along with user created pipelines
|
||||
defaultPipelines, err := pc.getDefaultPipelines()
|
||||
if err != nil {
|
||||
return nil, "", model.InternalError(fmt.Errorf("failed to get default pipelines: %w", err))
|
||||
if querybuilder.BodyJSONQueryEnabled {
|
||||
// add default normalize pipeline at the beginning
|
||||
pipelinesResp.Pipelines = append([]pipelinetypes.GettablePipeline{pc.getNormalizePipeline()}, pipelinesResp.Pipelines...)
|
||||
}
|
||||
pipelinesResp.Pipelines = append(pipelinesResp.Pipelines, defaultPipelines...)
|
||||
|
||||
updatedConf, err := GenerateCollectorConfigWithPipelines(currentConfYaml, pipelinesResp.Pipelines)
|
||||
if err != nil {
|
||||
|
||||
@@ -7,14 +7,12 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"log/slog"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
opentracing "github.com/opentracing/opentracing-go"
|
||||
plabels "github.com/prometheus/prometheus/model/labels"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// PromRuleTask is a promql rule executor
|
||||
@@ -373,7 +371,7 @@ func (g *PromRuleTask) Eval(ctx context.Context, ts time.Time) {
|
||||
|
||||
comment := ctxtypes.CommentFromContext(ctx)
|
||||
comment.Set("rule_id", rule.ID())
|
||||
comment.Set("identn_provider", authtypes.IdentNProviderInternal.StringValue())
|
||||
comment.Set("auth_type", "internal")
|
||||
ctx = ctxtypes.NewContextWithComment(ctx, comment)
|
||||
|
||||
_, err := rule.Eval(ctx, ts)
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -359,7 +358,7 @@ func (g *RuleTask) Eval(ctx context.Context, ts time.Time) {
|
||||
|
||||
comment := ctxtypes.CommentFromContext(ctx)
|
||||
comment.Set("rule_id", rule.ID())
|
||||
comment.Set("identn_provider", authtypes.IdentNProviderInternal.StringValue())
|
||||
comment.Set("auth_type", "internal")
|
||||
ctx = ctxtypes.NewContextWithComment(ctx, comment)
|
||||
|
||||
_, err := rule.Eval(ctx, ts)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user