mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-17 06:32:10 +00:00
Compare commits
24 Commits
fix/root-u
...
sso-mappin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
634945877e | ||
|
|
acdd5dd6b2 | ||
|
|
78cb726414 | ||
|
|
aa39db8ac2 | ||
|
|
e4e91d1b43 | ||
|
|
e5929e4fc4 | ||
|
|
eb2c6b78c8 | ||
|
|
2d2d0c3d9f | ||
|
|
8a4544cbac | ||
|
|
5520e8086e | ||
|
|
3f5544674e | ||
|
|
c638e39ccc | ||
|
|
e5840b2193 | ||
|
|
ceac0f3c14 | ||
|
|
b61628e00e | ||
|
|
b621e2470d | ||
|
|
a75ee7bdba | ||
|
|
1869e91155 | ||
|
|
e10c29399a | ||
|
|
9a89cb02b0 | ||
|
|
f41eb6f5b8 | ||
|
|
cad51403e5 | ||
|
|
370952b7f9 | ||
|
|
c57ab4d76a |
@@ -1,4 +1,4 @@
|
||||
FROM node:22-bookworm AS build
|
||||
FROM node:18-bullseye AS build
|
||||
|
||||
WORKDIR /opt/
|
||||
COPY ./frontend/ ./
|
||||
|
||||
@@ -309,14 +309,3 @@ user:
|
||||
allow_self: true
|
||||
# The duration within which a user can reset their password.
|
||||
max_token_lifetime: 6h
|
||||
root:
|
||||
# Whether to enable the root user. When enabled, a root user is provisioned
|
||||
# on startup using the email and password below. The root user cannot be
|
||||
# deleted, updated, or have their password changed through the UI.
|
||||
enabled: false
|
||||
# The email address of the root user.
|
||||
email: ""
|
||||
# The password of the root user. Must meet password requirements.
|
||||
password: ""
|
||||
# The name of the organization to create or look up for the root user.
|
||||
org_name: default
|
||||
|
||||
@@ -4678,8 +4678,6 @@ components:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
isRoot:
|
||||
type: boolean
|
||||
orgId:
|
||||
type: string
|
||||
role:
|
||||
|
||||
@@ -45,7 +45,7 @@ type APIHandler struct {
|
||||
}
|
||||
|
||||
// NewAPIHandler returns an APIHandler
|
||||
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.Config) (*APIHandler, error) {
|
||||
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, error) {
|
||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||
Reader: opts.DataConnector,
|
||||
RuleManager: opts.RulesManager,
|
||||
@@ -58,7 +58,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
|
||||
Signoz: signoz,
|
||||
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier, signoz.Analytics),
|
||||
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
|
||||
}, config)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -175,7 +175,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
GlobalConfig: config.Global,
|
||||
}
|
||||
|
||||
apiHandler, err := api.NewAPIHandler(apiOpts, signoz, config)
|
||||
apiHandler, err := api.NewAPIHandler(apiOpts, signoz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -55,8 +55,10 @@
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/input": "0.0.2",
|
||||
"@signozhq/popover": "0.0.0",
|
||||
"@signozhq/radio-group": "0.0.2",
|
||||
"@signozhq/resizable": "0.0.0",
|
||||
"@signozhq/sonner": "0.1.0",
|
||||
"@signozhq/switch": "0.0.2",
|
||||
"@signozhq/table": "0.3.7",
|
||||
"@signozhq/tooltip": "0.0.2",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
|
||||
@@ -1542,10 +1542,6 @@ export interface TypesUserDTO {
|
||||
* @type string
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
isRoot?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
|
||||
const deleteDomain = async (id: string): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.delete<null>(`/domains/${id}`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteDomain;
|
||||
@@ -1,25 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||
import { UpdatableAuthDomain } from 'types/api/v1/domains/put';
|
||||
|
||||
const put = async (
|
||||
props: UpdatableAuthDomain,
|
||||
): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.put<RawSuccessResponse<null>>(
|
||||
`/domains/${props.id}`,
|
||||
{ config: props.config },
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default put;
|
||||
@@ -1,24 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||
import { GettableAuthDomain } from 'types/api/v1/domains/list';
|
||||
|
||||
const listAllDomain = async (): Promise<
|
||||
SuccessResponseV2<GettableAuthDomain[]>
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get<RawSuccessResponse<GettableAuthDomain[]>>(
|
||||
`/domains`,
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default listAllDomain;
|
||||
@@ -1,26 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||
import { GettableAuthDomain } from 'types/api/v1/domains/list';
|
||||
import { PostableAuthDomain } from 'types/api/v1/domains/post';
|
||||
|
||||
const post = async (
|
||||
props: PostableAuthDomain,
|
||||
): Promise<SuccessResponseV2<GettableAuthDomain>> => {
|
||||
try {
|
||||
const response = await axios.post<RawSuccessResponse<GettableAuthDomain>>(
|
||||
`/domains`,
|
||||
props,
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default post;
|
||||
2
frontend/src/auto-import-registry.d.ts
vendored
2
frontend/src/auto-import-registry.d.ts
vendored
@@ -21,7 +21,9 @@ import '@signozhq/design-tokens';
|
||||
import '@signozhq/icons';
|
||||
import '@signozhq/input';
|
||||
import '@signozhq/popover';
|
||||
import '@signozhq/radio-group';
|
||||
import '@signozhq/resizable';
|
||||
import '@signozhq/sonner';
|
||||
import '@signozhq/switch';
|
||||
import '@signozhq/table';
|
||||
import '@signozhq/tooltip';
|
||||
|
||||
@@ -14,11 +14,6 @@ export interface GraphVisibilityState {
|
||||
dataIndex: SeriesVisibilityItem[];
|
||||
}
|
||||
|
||||
export interface SeriesVisibilityState {
|
||||
labels: string[];
|
||||
visibility: boolean[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Context in which a panel is rendered. Used to vary behavior (e.g. persistence,
|
||||
* interactions) per context.
|
||||
|
||||
@@ -62,10 +62,10 @@ describe('legendVisibilityUtils', () => {
|
||||
const result = getStoredSeriesVisibility('widget-1');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
labels: ['CPU', 'Memory'],
|
||||
visibility: [true, false],
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{ label: 'CPU', show: true },
|
||||
{ label: 'Memory', show: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns visibility by index including duplicate labels', () => {
|
||||
@@ -85,10 +85,11 @@ describe('legendVisibilityUtils', () => {
|
||||
const result = getStoredSeriesVisibility('widget-1');
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toEqual({
|
||||
labels: ['CPU', 'CPU', 'Memory'],
|
||||
visibility: [true, false, false],
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{ label: 'CPU', show: true },
|
||||
{ label: 'CPU', show: false },
|
||||
{ label: 'Memory', show: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns null on malformed JSON in localStorage', () => {
|
||||
@@ -127,10 +128,10 @@ describe('legendVisibilityUtils', () => {
|
||||
const stored = getStoredSeriesVisibility('widget-1');
|
||||
|
||||
expect(stored).not.toBeNull();
|
||||
expect(stored).toEqual({
|
||||
labels: ['CPU', 'Memory'],
|
||||
visibility: [true, false],
|
||||
});
|
||||
expect(stored).toEqual([
|
||||
{ label: 'CPU', show: true },
|
||||
{ label: 'Memory', show: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds a new widget entry when other widgets already exist', () => {
|
||||
@@ -149,7 +150,7 @@ describe('legendVisibilityUtils', () => {
|
||||
const stored = getStoredSeriesVisibility('widget-new');
|
||||
|
||||
expect(stored).not.toBeNull();
|
||||
expect(stored).toEqual({ labels: ['CPU'], visibility: [false] });
|
||||
expect(stored).toEqual([{ label: 'CPU', show: false }]);
|
||||
});
|
||||
|
||||
it('updates existing widget visibility when entry already exists', () => {
|
||||
@@ -175,10 +176,10 @@ describe('legendVisibilityUtils', () => {
|
||||
const stored = getStoredSeriesVisibility('widget-1');
|
||||
|
||||
expect(stored).not.toBeNull();
|
||||
expect(stored).toEqual({
|
||||
labels: ['CPU', 'Memory'],
|
||||
visibility: [false, true],
|
||||
});
|
||||
expect(stored).toEqual([
|
||||
{ label: 'CPU', show: false },
|
||||
{ label: 'Memory', show: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it('silently handles malformed existing JSON without throwing', () => {
|
||||
@@ -201,10 +202,10 @@ describe('legendVisibilityUtils', () => {
|
||||
|
||||
const stored = getStoredSeriesVisibility('widget-1');
|
||||
expect(stored).not.toBeNull();
|
||||
expect(stored).toEqual({
|
||||
labels: ['x-axis', 'CPU'],
|
||||
visibility: [true, false],
|
||||
});
|
||||
expect(stored).toEqual([
|
||||
{ label: 'x-axis', show: true },
|
||||
{ label: 'CPU', show: false },
|
||||
]);
|
||||
const expected = [
|
||||
{
|
||||
name: 'widget-1',
|
||||
@@ -231,14 +232,12 @@ describe('legendVisibilityUtils', () => {
|
||||
{ label: 'B', show: true },
|
||||
]);
|
||||
|
||||
expect(getStoredSeriesVisibility('widget-a')).toEqual({
|
||||
labels: ['A'],
|
||||
visibility: [true],
|
||||
});
|
||||
expect(getStoredSeriesVisibility('widget-b')).toEqual({
|
||||
labels: ['B'],
|
||||
visibility: [true],
|
||||
});
|
||||
expect(getStoredSeriesVisibility('widget-a')).toEqual([
|
||||
{ label: 'A', show: true },
|
||||
]);
|
||||
expect(getStoredSeriesVisibility('widget-b')).toEqual([
|
||||
{ label: 'B', show: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it('calls setItem with storage key and stringified visibility states', () => {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
|
||||
import {
|
||||
GraphVisibilityState,
|
||||
SeriesVisibilityItem,
|
||||
SeriesVisibilityState,
|
||||
} from '../types';
|
||||
import { GraphVisibilityState, SeriesVisibilityItem } from '../types';
|
||||
|
||||
/**
|
||||
* Retrieves the stored series visibility for a specific widget from localStorage by index.
|
||||
@@ -14,7 +10,7 @@ import {
|
||||
*/
|
||||
export function getStoredSeriesVisibility(
|
||||
widgetId: string,
|
||||
): SeriesVisibilityState | null {
|
||||
): SeriesVisibilityItem[] | null {
|
||||
try {
|
||||
const storedData = localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES);
|
||||
|
||||
@@ -29,10 +25,7 @@ export function getStoredSeriesVisibility(
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
labels: widgetState.dataIndex.map((item) => item.label),
|
||||
visibility: widgetState.dataIndex.map((item) => item.show),
|
||||
};
|
||||
return widgetState.dataIndex;
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
// If the stored data is malformed, remove it
|
||||
|
||||
@@ -477,47 +477,67 @@
|
||||
}
|
||||
}
|
||||
|
||||
.opentelemetry-radio-container {
|
||||
.observability-tools-radio-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0 12px;
|
||||
width: 528px;
|
||||
|
||||
.opentelemetry-radio-group {
|
||||
width: 100%;
|
||||
.observability-tool-radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
width: calc((528px - 12px) / 2);
|
||||
flex: 0 0 calc((528px - 12px) / 2);
|
||||
|
||||
.opentelemetry-radio-items-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
label {
|
||||
color: var(--l1-foreground);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.opentelemetry-radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
width: calc((528px - 12px) / 2);
|
||||
min-width: 258px;
|
||||
flex: 0 0 calc((528px - 12px) / 2);
|
||||
button[role='radio'] {
|
||||
&[data-state='unchecked'] {
|
||||
border-color: var(--l3-border) !important;
|
||||
border-width: 1px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.observability-tool-others-item {
|
||||
.onboarding-questionaire-other-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.migration-timeline-radio-container,
|
||||
.opentelemetry-radio-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0 12px;
|
||||
width: 528px;
|
||||
|
||||
.migration-timeline-radio-item,
|
||||
.opentelemetry-radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
width: calc((528px - 12px) / 2);
|
||||
flex: 0 0 calc((528px - 12px) / 2);
|
||||
|
||||
label {
|
||||
color: var(--l1-foreground);
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.065px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ant-radio {
|
||||
.ant-radio-inner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-color: var(--l3-border);
|
||||
}
|
||||
|
||||
&.ant-radio-checked .ant-radio-inner {
|
||||
border-color: var(--bg-robin-500);
|
||||
background-color: var(--bg-robin-500);
|
||||
}
|
||||
button[role='radio'] {
|
||||
&[data-state='unchecked'] {
|
||||
border-color: var(--l3-border) !important;
|
||||
border-width: 1px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -977,27 +997,6 @@
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
.opentelemetry-radio-container {
|
||||
.opentelemetry-radio-group {
|
||||
.opentelemetry-radio-items-wrapper {
|
||||
.opentelemetry-radio-item {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
.ant-radio {
|
||||
.ant-radio-inner {
|
||||
border-color: var(--l3-border);
|
||||
}
|
||||
|
||||
&.ant-radio-checked .ant-radio-inner {
|
||||
border-color: var(--bg-robin-500);
|
||||
background-color: var(--bg-robin-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-back-button {
|
||||
border-color: var(--text-vanilla-300);
|
||||
color: var(--l3-foreground);
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Checkbox } from '@signozhq/checkbox';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Radio, Typography } from 'antd';
|
||||
import { RadioChangeEvent } from 'antd/es/radio';
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
RadioGroupLabel,
|
||||
} from '@signozhq/radio-group';
|
||||
import { Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import editOrg from 'api/organization/editOrg';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { ArrowRight, Loader2 } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
import '../OnboardingQuestionaire.styles.scss';
|
||||
|
||||
export interface OrgData {
|
||||
id: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface OrgDetails {
|
||||
organisationName: string;
|
||||
usesObservability: boolean | null;
|
||||
observabilityTool: string | null;
|
||||
otherTool: string | null;
|
||||
usesOtel: boolean | null;
|
||||
migrationTimeline: string | null;
|
||||
}
|
||||
|
||||
interface OrgQuestionsProps {
|
||||
currentOrgData: OrgData | null;
|
||||
orgDetails: OrgDetails;
|
||||
onNext: (details: OrgDetails) => void;
|
||||
}
|
||||
@@ -45,19 +38,14 @@ const observabilityTools = {
|
||||
Others: 'Others',
|
||||
};
|
||||
|
||||
function OrgQuestions({
|
||||
currentOrgData,
|
||||
orgDetails,
|
||||
onNext,
|
||||
}: OrgQuestionsProps): JSX.Element {
|
||||
const { updateOrg } = useAppContext();
|
||||
const { notifications } = useNotifications();
|
||||
const migrationTimelineOptions = {
|
||||
lessThanMonth: 'Less than a month',
|
||||
oneToThreeMonths: '1-3 months',
|
||||
greaterThanThreeMonths: 'Greater than 3 months',
|
||||
justExploring: 'Just exploring',
|
||||
};
|
||||
|
||||
const { t } = useTranslation(['organizationsettings', 'common']);
|
||||
|
||||
const [organisationName, setOrganisationName] = useState<string>(
|
||||
orgDetails?.organisationName || '',
|
||||
);
|
||||
function OrgQuestions({ orgDetails, onNext }: OrgQuestionsProps): JSX.Element {
|
||||
const [observabilityTool, setObservabilityTool] = useState<string | null>(
|
||||
orgDetails?.observabilityTool || null,
|
||||
);
|
||||
@@ -66,92 +54,33 @@ function OrgQuestions({
|
||||
);
|
||||
const [isNextDisabled, setIsNextDisabled] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
setOrganisationName(orgDetails.organisationName);
|
||||
}, [orgDetails.organisationName]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [usesOtel, setUsesOtel] = useState<boolean | null>(orgDetails.usesOtel);
|
||||
const [migrationTimeline, setMigrationTimeline] = useState<string | null>(
|
||||
orgDetails?.migrationTimeline || null,
|
||||
);
|
||||
|
||||
const handleOrgNameUpdate = async (): Promise<void> => {
|
||||
const showMigrationQuestion =
|
||||
observabilityTool !== null && observabilityTool !== 'None';
|
||||
|
||||
const handleNext = (): void => {
|
||||
const usesObservability =
|
||||
!observabilityTool?.includes('None') && observabilityTool !== null;
|
||||
|
||||
/* Early bailout if orgData is not set or if the organisation name is not set or if the organisation name is empty or if the organisation name is the same as the one in the orgData */
|
||||
if (
|
||||
!currentOrgData ||
|
||||
!organisationName ||
|
||||
organisationName === '' ||
|
||||
orgDetails.organisationName === organisationName
|
||||
) {
|
||||
logEvent('Org Onboarding: Answered', {
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
usesOtel,
|
||||
});
|
||||
logEvent('Org Onboarding: Answered', {
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
usesOtel,
|
||||
migrationTimeline,
|
||||
});
|
||||
|
||||
onNext({
|
||||
organisationName,
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
usesOtel,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const { statusCode, error } = await editOrg({
|
||||
displayName: organisationName,
|
||||
orgId: currentOrgData.id,
|
||||
});
|
||||
if (statusCode === 204) {
|
||||
updateOrg(currentOrgData?.id, organisationName);
|
||||
|
||||
logEvent('Org Onboarding: Org Name Updated', {
|
||||
organisationName,
|
||||
});
|
||||
|
||||
logEvent('Org Onboarding: Answered', {
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
usesOtel,
|
||||
});
|
||||
|
||||
onNext({
|
||||
organisationName,
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
usesOtel,
|
||||
});
|
||||
} else {
|
||||
logEvent('Org Onboarding: Org Name Update Failed', {
|
||||
organisationName: orgDetails.organisationName,
|
||||
});
|
||||
|
||||
notifications.error({
|
||||
message:
|
||||
error ||
|
||||
t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
notifications.error({
|
||||
message: t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
onNext({
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
usesOtel,
|
||||
migrationTimeline,
|
||||
});
|
||||
};
|
||||
|
||||
const isValidUsesObservability = (): boolean => {
|
||||
@@ -173,22 +102,29 @@ function OrgQuestions({
|
||||
|
||||
useEffect(() => {
|
||||
const isValidObservability = isValidUsesObservability();
|
||||
const isMigrationValid = !showMigrationQuestion || migrationTimeline !== null;
|
||||
|
||||
if (organisationName !== '' && usesOtel !== null && isValidObservability) {
|
||||
if (usesOtel !== null && isValidObservability && isMigrationValid) {
|
||||
setIsNextDisabled(false);
|
||||
} else {
|
||||
setIsNextDisabled(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [organisationName, usesOtel, observabilityTool, otherTool]);
|
||||
}, [
|
||||
usesOtel,
|
||||
observabilityTool,
|
||||
otherTool,
|
||||
migrationTimeline,
|
||||
showMigrationQuestion,
|
||||
]);
|
||||
|
||||
const createObservabilityToolHandler = (tool: string) => (
|
||||
checked: boolean,
|
||||
): void => {
|
||||
if (checked) {
|
||||
setObservabilityTool(tool);
|
||||
} else if (observabilityTool === tool) {
|
||||
setObservabilityTool(null);
|
||||
const handleObservabilityToolChange = (value: string): void => {
|
||||
setObservabilityTool(value);
|
||||
if (value !== 'Others') {
|
||||
setOtherTool('');
|
||||
}
|
||||
if (value === 'None') {
|
||||
setMigrationTimeline(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -196,10 +132,6 @@ function OrgQuestions({
|
||||
setUsesOtel(value === 'yes');
|
||||
};
|
||||
|
||||
const handleOnNext = (): void => {
|
||||
handleOrgNameUpdate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="questions-container">
|
||||
<div className="onboarding-header-section">
|
||||
@@ -214,40 +146,24 @@ function OrgQuestions({
|
||||
|
||||
<div className="questions-form-container">
|
||||
<div className="questions-form">
|
||||
<div className="form-group">
|
||||
<label className="question" htmlFor="organisationName">
|
||||
Name of your company
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
name="organisationName"
|
||||
id="organisationName"
|
||||
placeholder="e.g. Simpsonville"
|
||||
autoComplete="off"
|
||||
value={organisationName}
|
||||
onChange={(e): void => setOrganisationName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="question" htmlFor="observabilityTool">
|
||||
Which observability tool do you currently use?
|
||||
</label>
|
||||
<div className="observability-tools-checkbox-container">
|
||||
<RadioGroup
|
||||
value={observabilityTool || ''}
|
||||
onValueChange={handleObservabilityToolChange}
|
||||
className="observability-tools-radio-container"
|
||||
>
|
||||
{Object.entries(observabilityTools).map(([tool, label]) => {
|
||||
if (tool === 'Others') {
|
||||
return (
|
||||
<div
|
||||
key={tool}
|
||||
className="checkbox-item observability-tool-checkbox-item observability-tool-others-item"
|
||||
className="radio-item observability-tool-radio-item observability-tool-others-item"
|
||||
>
|
||||
<Checkbox
|
||||
id={`checkbox-${tool}`}
|
||||
checked={observabilityTool === tool}
|
||||
onCheckedChange={createObservabilityToolHandler(tool)}
|
||||
labelName={observabilityTool === 'Others' ? '' : label}
|
||||
/>
|
||||
{observabilityTool === 'Others' && (
|
||||
<RadioGroupItem value={tool} id={`radio-${tool}`} />
|
||||
{observabilityTool === 'Others' ? (
|
||||
<Input
|
||||
type="text"
|
||||
className="onboarding-questionaire-other-input"
|
||||
@@ -256,55 +172,60 @@ function OrgQuestions({
|
||||
autoFocus
|
||||
onChange={(e): void => setOtherTool(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<RadioGroupLabel htmlFor={`radio-${tool}`}>{label}</RadioGroupLabel>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={tool}
|
||||
className="checkbox-item observability-tool-checkbox-item"
|
||||
>
|
||||
<Checkbox
|
||||
id={`checkbox-${tool}`}
|
||||
checked={observabilityTool === tool}
|
||||
onCheckedChange={createObservabilityToolHandler(tool)}
|
||||
labelName={label}
|
||||
/>
|
||||
<div key={tool} className="radio-item observability-tool-radio-item">
|
||||
<RadioGroupItem value={tool} id={`radio-${tool}`} />
|
||||
<RadioGroupLabel htmlFor={`radio-${tool}`}>{label}</RadioGroupLabel>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{showMigrationQuestion && (
|
||||
<div className="form-group">
|
||||
<div className="question">
|
||||
What is your timeline for migrating to SigNoz?
|
||||
</div>
|
||||
<RadioGroup
|
||||
value={migrationTimeline || ''}
|
||||
onValueChange={setMigrationTimeline}
|
||||
className="migration-timeline-radio-container"
|
||||
>
|
||||
{Object.entries(migrationTimelineOptions).map(([key, label]) => (
|
||||
<div key={key} className="radio-item migration-timeline-radio-item">
|
||||
<RadioGroupItem value={key} id={`radio-migration-${key}`} />
|
||||
<RadioGroupLabel htmlFor={`radio-migration-${key}`}>
|
||||
{label}
|
||||
</RadioGroupLabel>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<div className="question">Do you already use OpenTelemetry?</div>
|
||||
<div className="opentelemetry-radio-container">
|
||||
<Radio.Group
|
||||
value={((): string | undefined => {
|
||||
if (usesOtel === true) {
|
||||
return 'yes';
|
||||
}
|
||||
if (usesOtel === false) {
|
||||
return 'no';
|
||||
}
|
||||
return undefined;
|
||||
})()}
|
||||
onChange={(e: RadioChangeEvent): void =>
|
||||
handleOtelChange(e.target.value)
|
||||
}
|
||||
className="opentelemetry-radio-group"
|
||||
>
|
||||
<div className="opentelemetry-radio-items-wrapper">
|
||||
<Radio value="yes" className="opentelemetry-radio-item">
|
||||
Yes
|
||||
</Radio>
|
||||
<Radio value="no" className="opentelemetry-radio-item">
|
||||
No
|
||||
</Radio>
|
||||
</div>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
<RadioGroup
|
||||
value={usesOtel === true ? 'yes' : usesOtel === false ? 'no' : ''}
|
||||
onValueChange={handleOtelChange}
|
||||
className="opentelemetry-radio-container"
|
||||
>
|
||||
<div className="radio-item opentelemetry-radio-item">
|
||||
<RadioGroupItem value="yes" id="radio-otel-yes" />
|
||||
<RadioGroupLabel htmlFor="radio-otel-yes">Yes</RadioGroupLabel>
|
||||
</div>
|
||||
<div className="radio-item opentelemetry-radio-item">
|
||||
<RadioGroupItem value="no" id="radio-otel-no" />
|
||||
<RadioGroupLabel htmlFor="radio-otel-no">No</RadioGroupLabel>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -312,15 +233,9 @@ function OrgQuestions({
|
||||
variant="solid"
|
||||
color="primary"
|
||||
className={`onboarding-next-button ${isNextDisabled ? 'disabled' : ''}`}
|
||||
onClick={handleOnNext}
|
||||
onClick={handleNext}
|
||||
disabled={isNextDisabled}
|
||||
suffixIcon={
|
||||
isLoading ? (
|
||||
<Loader2 className="animate-spin" size={12} />
|
||||
) : (
|
||||
<ArrowRight size={12} />
|
||||
)
|
||||
}
|
||||
suffixIcon={<ArrowRight size={12} />}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('OnboardingQuestionaire Component', () => {
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
expect(screen.getByText(/welcome to signoz cloud/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/name of your company/i)).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByText(/which observability tool do you currently use/i),
|
||||
).toBeInTheDocument();
|
||||
@@ -86,15 +86,12 @@ describe('OnboardingQuestionaire Component', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
|
||||
const datadogCheckbox = screen.getByLabelText(/datadog/i);
|
||||
await user.click(datadogCheckbox);
|
||||
|
||||
const otelYes = screen.getByRole('radio', { name: /yes/i });
|
||||
await user.click(otelYes);
|
||||
await user.click(screen.getByLabelText(/just exploring/i));
|
||||
|
||||
const nextButton = await screen.findByRole('button', { name: /next/i });
|
||||
expect(nextButton).not.toBeDisabled();
|
||||
@@ -112,15 +109,38 @@ describe('OnboardingQuestionaire Component', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows migration timeline options only when specific observability tools are selected', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Initially not visible
|
||||
expect(
|
||||
screen.queryByText(/What is your timeline for migrating to SigNoz/i),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const datadogCheckbox = screen.getByLabelText(/datadog/i);
|
||||
await user.click(datadogCheckbox);
|
||||
|
||||
expect(
|
||||
await screen.findByText(/What is your timeline for migrating to SigNoz/i),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Not visible when None is selected
|
||||
const noneCheckbox = screen.getByLabelText(/none\/starting fresh/i);
|
||||
await user.click(noneCheckbox);
|
||||
|
||||
expect(
|
||||
screen.queryByText(/What is your timeline for migrating to SigNoz/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('proceeds to step 2 when next is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByLabelText(/just exploring/i));
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: /next/i });
|
||||
await user.click(nextButton);
|
||||
@@ -137,11 +157,10 @@ describe('OnboardingQuestionaire Component', () => {
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Navigate to step 2
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByLabelText(/just exploring/i));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
expect(
|
||||
@@ -157,11 +176,10 @@ describe('OnboardingQuestionaire Component', () => {
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Navigate to step 2
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByLabelText(/just exploring/i));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -175,11 +193,10 @@ describe('OnboardingQuestionaire Component', () => {
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Navigate to step 2
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByLabelText(/just exploring/i));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
expect(
|
||||
@@ -203,11 +220,10 @@ describe('OnboardingQuestionaire Component', () => {
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Navigate to step 2
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByLabelText(/just exploring/i));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
expect(
|
||||
@@ -232,11 +248,10 @@ describe('OnboardingQuestionaire Component', () => {
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Navigate through steps 1 and 2
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByLabelText(/just exploring/i));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
expect(
|
||||
@@ -267,11 +282,10 @@ describe('OnboardingQuestionaire Component', () => {
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
// Navigate to step 3
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
await user.click(screen.getByLabelText(/just exploring/i));
|
||||
await user.click(screen.getByRole('button', { name: /next/i }));
|
||||
|
||||
expect(
|
||||
@@ -290,40 +304,4 @@ describe('OnboardingQuestionaire Component', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('handles organization update error gracefully', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.put(EDIT_ORG_ENDPOINT, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(500),
|
||||
ctx.json({
|
||||
error: {
|
||||
code: 'INTERNAL_ERROR',
|
||||
message: 'Failed to update organization',
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<OnboardingQuestionaire />);
|
||||
|
||||
const orgNameInput = screen.getByLabelText(/name of your company/i);
|
||||
await user.clear(orgNameInput);
|
||||
await user.type(orgNameInput, 'Test Company');
|
||||
await user.click(screen.getByLabelText(/datadog/i));
|
||||
await user.click(screen.getByRole('radio', { name: /yes/i }));
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: /next/i });
|
||||
await user.click(nextButton);
|
||||
|
||||
// Component should still be functional
|
||||
await waitFor(() => {
|
||||
expect(nextButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ import InviteTeamMembers from './InviteTeamMembers/InviteTeamMembers';
|
||||
import OptimiseSignozNeeds, {
|
||||
OptimiseSignozDetails,
|
||||
} from './OptimiseSignozNeeds/OptimiseSignozNeeds';
|
||||
import OrgQuestions, { OrgData, OrgDetails } from './OrgQuestions/OrgQuestions';
|
||||
import OrgQuestions, { OrgDetails } from './OrgQuestions/OrgQuestions';
|
||||
|
||||
import './OnboardingQuestionaire.styles.scss';
|
||||
|
||||
@@ -37,11 +37,11 @@ export const showErrorNotification = (
|
||||
};
|
||||
|
||||
const INITIAL_ORG_DETAILS: OrgDetails = {
|
||||
organisationName: '',
|
||||
usesObservability: true,
|
||||
observabilityTool: '',
|
||||
otherTool: '',
|
||||
usesOtel: null,
|
||||
migrationTimeline: null,
|
||||
};
|
||||
|
||||
const INITIAL_SIGNOZ_DETAILS: SignozDetails = {
|
||||
@@ -79,25 +79,11 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
InviteTeamMembersProps[] | null
|
||||
>(null);
|
||||
|
||||
const [currentOrgData, setCurrentOrgData] = useState<OrgData | null>(null);
|
||||
|
||||
const [
|
||||
updatingOrgOnboardingStatus,
|
||||
setUpdatingOrgOnboardingStatus,
|
||||
] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (org) {
|
||||
setCurrentOrgData(org[0]);
|
||||
|
||||
setOrgDetails({
|
||||
...orgDetails,
|
||||
organisationName: org[0].displayName,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [org]);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent('Org Onboarding: Started', {
|
||||
org_id: org?.[0]?.id,
|
||||
@@ -175,6 +161,7 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
? (orgDetails?.otherTool as string)
|
||||
: (orgDetails?.observabilityTool as string),
|
||||
where_did_you_discover_signoz: signozDetails?.discoverSignoz as string,
|
||||
timeline_for_migrating_to_signoz: orgDetails?.migrationTimeline as string,
|
||||
reasons_for_interest_in_signoz: signozDetails?.interestInSignoz?.includes(
|
||||
'Others',
|
||||
)
|
||||
@@ -208,7 +195,6 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
<div className="onboarding-questionaire-content">
|
||||
{currentStep === 1 && (
|
||||
<OrgQuestions
|
||||
currentOrgData={currentOrgData}
|
||||
orgDetails={{
|
||||
...orgDetails,
|
||||
usesOtel: orgDetails.usesOtel ?? null,
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.auth-domain-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +21,29 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24px;
|
||||
|
||||
.auth-domain-list-action-link {
|
||||
cursor: pointer;
|
||||
color: var(--bg-robin-500);
|
||||
transition: color 0.3s;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.delete {
|
||||
color: var(--bg-cherry-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-domain-list-na {
|
||||
padding-left: 6px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Button, Form, Modal } from 'antd';
|
||||
import put from 'api/v1/domains/id/put';
|
||||
import post from 'api/v1/domains/post';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import {
|
||||
useCreateAuthDomain,
|
||||
useUpdateAuthDomain,
|
||||
} from 'api/generated/services/authdomains';
|
||||
import {
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
AuthtypesGoogleConfigDTO,
|
||||
AuthtypesOIDCConfigDTO,
|
||||
AuthtypesPostableAuthDomainDTO,
|
||||
AuthtypesRoleMappingDTO,
|
||||
AuthtypesSamlConfigDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { GettableAuthDomain } from 'types/api/v1/domains/list';
|
||||
import { PostableAuthDomain } from 'types/api/v1/domains/post';
|
||||
|
||||
import AuthnProviderSelector from './AuthnProviderSelector';
|
||||
import ConfigureGoogleAuthAuthnProvider from './Providers/AuthnGoogleAuth';
|
||||
@@ -20,7 +33,22 @@ import './CreateEdit.styles.scss';
|
||||
interface CreateOrEditProps {
|
||||
isCreate: boolean;
|
||||
onClose: () => void;
|
||||
record?: GettableAuthDomain;
|
||||
record?: AuthtypesGettableAuthDomainDTO;
|
||||
}
|
||||
|
||||
// Form values interface for internal use (includes array-based fields for UI)
|
||||
interface FormValues {
|
||||
name?: string;
|
||||
ssoEnabled?: boolean;
|
||||
ssoType?: string;
|
||||
googleAuthConfig?: AuthtypesGoogleConfigDTO & {
|
||||
domainToAdminEmailList?: Array<{ domain?: string; adminEmail?: string }>;
|
||||
};
|
||||
samlConfig?: AuthtypesSamlConfigDTO;
|
||||
oidcConfig?: AuthtypesOIDCConfigDTO;
|
||||
roleMapping?: AuthtypesRoleMappingDTO & {
|
||||
groupMappingsList?: Array<{ groupName?: string; role?: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
function configureAuthnProvider(
|
||||
@@ -39,64 +67,299 @@ function configureAuthnProvider(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts groupMappingsList array to groupMappings Record for API
|
||||
*/
|
||||
function convertGroupMappingsToRecord(
|
||||
groupMappingsList?: Array<{ groupName?: string; role?: string }>,
|
||||
): Record<string, string> | undefined {
|
||||
if (!Array.isArray(groupMappingsList) || groupMappingsList.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const groupMappings: Record<string, string> = {};
|
||||
groupMappingsList.forEach((item) => {
|
||||
if (item.groupName && item.role) {
|
||||
groupMappings[item.groupName] = item.role;
|
||||
}
|
||||
});
|
||||
|
||||
return Object.keys(groupMappings).length > 0 ? groupMappings : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts groupMappings Record to groupMappingsList array for form
|
||||
*/
|
||||
function convertGroupMappingsToList(
|
||||
groupMappings?: Record<string, string> | null,
|
||||
): Array<{ groupName: string; role: string }> {
|
||||
if (!groupMappings) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(groupMappings).map(([groupName, role]) => ({
|
||||
groupName,
|
||||
role,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts domainToAdminEmailList array to domainToAdminEmail Record for API
|
||||
*/
|
||||
function convertDomainMappingsToRecord(
|
||||
domainToAdminEmailList?: Array<{ domain?: string; adminEmail?: string }>,
|
||||
): Record<string, string> | undefined {
|
||||
if (
|
||||
!Array.isArray(domainToAdminEmailList) ||
|
||||
domainToAdminEmailList.length === 0
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const domainToAdminEmail: Record<string, string> = {};
|
||||
domainToAdminEmailList.forEach((item) => {
|
||||
if (item.domain && item.adminEmail) {
|
||||
domainToAdminEmail[item.domain] = item.adminEmail;
|
||||
}
|
||||
});
|
||||
|
||||
return Object.keys(domainToAdminEmail).length > 0
|
||||
? domainToAdminEmail
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts domainToAdminEmail Record to domainToAdminEmailList array for form
|
||||
*/
|
||||
function convertDomainMappingsToList(
|
||||
domainToAdminEmail?: Record<string, string>,
|
||||
): Array<{ domain: string; adminEmail: string }> {
|
||||
if (!domainToAdminEmail) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(domainToAdminEmail).map(([domain, adminEmail]) => ({
|
||||
domain,
|
||||
adminEmail,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares initial form values from API record
|
||||
*/
|
||||
function prepareInitialValues(
|
||||
record?: AuthtypesGettableAuthDomainDTO,
|
||||
): FormValues {
|
||||
if (!record) {
|
||||
return {
|
||||
name: '',
|
||||
ssoEnabled: false,
|
||||
ssoType: '',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
googleAuthConfig: record.googleAuthConfig
|
||||
? {
|
||||
...record.googleAuthConfig,
|
||||
domainToAdminEmailList: convertDomainMappingsToList(
|
||||
record.googleAuthConfig.domainToAdminEmail,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
roleMapping: record.roleMapping
|
||||
? {
|
||||
...record.roleMapping,
|
||||
groupMappingsList: convertGroupMappingsToList(
|
||||
record.roleMapping.groupMappings,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
const { isCreate, record, onClose } = props;
|
||||
const [form] = Form.useForm<PostableAuthDomain>();
|
||||
const [form] = Form.useForm<AuthtypesPostableAuthDomainDTO>();
|
||||
const [authnProvider, setAuthnProvider] = useState<string>(
|
||||
record?.ssoType || '',
|
||||
);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { featureFlags } = useAppContext();
|
||||
|
||||
const handleError = useCallback(
|
||||
(error: AxiosError<RenderErrorResponseDTO>): void => {
|
||||
try {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
} catch (apiError) {
|
||||
showErrorModal(apiError as APIError);
|
||||
}
|
||||
},
|
||||
[showErrorModal],
|
||||
);
|
||||
const samlEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.SSO)?.active || false;
|
||||
|
||||
const onSubmitHandler = async (): Promise<void> => {
|
||||
const {
|
||||
mutate: createAuthDomain,
|
||||
isLoading: isCreating,
|
||||
} = useCreateAuthDomain<AxiosError<RenderErrorResponseDTO>>();
|
||||
|
||||
const {
|
||||
mutate: updateAuthDomain,
|
||||
isLoading: isUpdating,
|
||||
} = useUpdateAuthDomain<AxiosError<RenderErrorResponseDTO>>();
|
||||
|
||||
/**
|
||||
* Prepares Google Auth config for API payload
|
||||
*/
|
||||
const getGoogleAuthConfig = useCallback(():
|
||||
| AuthtypesGoogleConfigDTO
|
||||
| undefined => {
|
||||
const config = form.getFieldValue('googleAuthConfig');
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { domainToAdminEmailList, ...rest } = config;
|
||||
const domainToAdminEmail = convertDomainMappingsToRecord(
|
||||
domainToAdminEmailList,
|
||||
);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...(domainToAdminEmail && { domainToAdminEmail }),
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
/**
|
||||
* Prepares role mapping for API payload
|
||||
*/
|
||||
const getRoleMapping = useCallback((): AuthtypesRoleMappingDTO | undefined => {
|
||||
const roleMapping = form.getFieldValue('roleMapping');
|
||||
if (!roleMapping) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { groupMappingsList, ...rest } = roleMapping;
|
||||
const groupMappings = convertGroupMappingsToRecord(groupMappingsList);
|
||||
|
||||
// Only return roleMapping if there's meaningful content
|
||||
const hasDefaultRole = !!rest.defaultRole;
|
||||
const hasUseRoleAttribute = rest.useRoleAttribute === true;
|
||||
const hasGroupMappings =
|
||||
groupMappings && Object.keys(groupMappings).length > 0;
|
||||
|
||||
if (!hasDefaultRole && !hasUseRoleAttribute && !hasGroupMappings) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...(groupMappings && { groupMappings }),
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
const onSubmitHandler = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = form.getFieldValue('name');
|
||||
const googleAuthConfig = form.getFieldValue('googleAuthConfig');
|
||||
const googleAuthConfig = getGoogleAuthConfig();
|
||||
const samlConfig = form.getFieldValue('samlConfig');
|
||||
const oidcConfig = form.getFieldValue('oidcConfig');
|
||||
const roleMapping = getRoleMapping();
|
||||
|
||||
try {
|
||||
if (isCreate) {
|
||||
await post({
|
||||
name,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: authnProvider,
|
||||
googleAuthConfig,
|
||||
samlConfig,
|
||||
oidcConfig,
|
||||
if (isCreate) {
|
||||
createAuthDomain(
|
||||
{
|
||||
data: {
|
||||
name,
|
||||
config: {
|
||||
ssoEnabled: true,
|
||||
ssoType: authnProvider,
|
||||
googleAuthConfig,
|
||||
samlConfig,
|
||||
oidcConfig,
|
||||
roleMapping,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await put({
|
||||
id: record?.id || '',
|
||||
config: {
|
||||
ssoEnabled: form.getFieldValue('ssoEnabled'),
|
||||
ssoType: authnProvider,
|
||||
googleAuthConfig,
|
||||
samlConfig,
|
||||
oidcConfig,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: 'Domain created successfully',
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
onError: handleError,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if (!record?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
updateAuthDomain(
|
||||
{
|
||||
pathParams: { id: record.id },
|
||||
data: {
|
||||
config: {
|
||||
ssoEnabled: form.getFieldValue('ssoEnabled'),
|
||||
ssoType: authnProvider,
|
||||
googleAuthConfig,
|
||||
samlConfig,
|
||||
oidcConfig,
|
||||
roleMapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: 'Domain updated successfully',
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: handleError,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
authnProvider,
|
||||
createAuthDomain,
|
||||
form,
|
||||
getGoogleAuthConfig,
|
||||
getRoleMapping,
|
||||
handleError,
|
||||
isCreate,
|
||||
notifications,
|
||||
onClose,
|
||||
record,
|
||||
updateAuthDomain,
|
||||
]);
|
||||
|
||||
const onBackHandler = (): void => {
|
||||
const onBackHandler = useCallback((): void => {
|
||||
form.resetFields();
|
||||
setAuthnProvider('');
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<Modal open footer={null} onCancel={onClose}>
|
||||
<Modal
|
||||
open
|
||||
footer={null}
|
||||
onCancel={onClose}
|
||||
width={authnProvider ? 980 : undefined}
|
||||
>
|
||||
<Form
|
||||
name="auth-domain"
|
||||
initialValues={defaultTo(record, {
|
||||
initialValues={defaultTo(prepareInitialValues(record), {
|
||||
name: '',
|
||||
ssoEnabled: false,
|
||||
ssoType: '',
|
||||
@@ -116,7 +379,11 @@ function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||
<section className="action-buttons">
|
||||
{isCreate && <Button onClick={onBackHandler}>Back</Button>}
|
||||
{!isCreate && <Button onClick={onClose}>Cancel</Button>}
|
||||
<Button onClick={onSubmitHandler} type="primary">
|
||||
<Button
|
||||
onClick={onSubmitHandler}
|
||||
type="primary"
|
||||
loading={isCreating || isUpdating}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
@@ -1,20 +1,67 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Callout } from '@signozhq/callout';
|
||||
import { Form, Input, Typography } from 'antd';
|
||||
import { Checkbox } from '@signozhq/checkbox';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleHelp,
|
||||
TriangleAlert,
|
||||
} from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Collapse, Form, Tooltip } from 'antd';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
|
||||
import DomainMappingList from './components/DomainMappingList';
|
||||
import EmailTagInput from './components/EmailTagInput';
|
||||
import { useCollapseSectionErrors } from './components/hooks/useCollapseSectionErrors';
|
||||
import RoleMappingSection from './components/RoleMappingSection';
|
||||
|
||||
import './Providers.styles.scss';
|
||||
|
||||
type ExpandedSection = 'workspace-groups' | 'role-mapping' | null;
|
||||
|
||||
function ConfigureGoogleAuthAuthnProvider({
|
||||
isCreate,
|
||||
}: {
|
||||
isCreate: boolean;
|
||||
}): JSX.Element {
|
||||
const form = Form.useFormInstance();
|
||||
const fetchGroups = Form.useWatch(['googleAuthConfig', 'fetchGroups'], form);
|
||||
|
||||
const [expandedSection, setExpandedSection] = useState<ExpandedSection>(null);
|
||||
|
||||
const handleWorkspaceGroupsChange = useCallback(
|
||||
(keys: string | string[]): void => {
|
||||
const isExpanding = Array.isArray(keys) ? keys.length > 0 : !!keys;
|
||||
setExpandedSection(isExpanding ? 'workspace-groups' : null);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRoleMappingChange = useCallback((expanded: boolean): void => {
|
||||
setExpandedSection(expanded ? 'role-mapping' : null);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
hasErrors: hasWorkspaceGroupsErrors,
|
||||
errorMessages: workspaceGroupsErrorMessages,
|
||||
} = useCollapseSectionErrors(
|
||||
['googleAuthConfig'],
|
||||
[
|
||||
['googleAuthConfig', 'fetchGroups'],
|
||||
['googleAuthConfig', 'serviceAccountJson'],
|
||||
['googleAuthConfig', 'domainToAdminEmailList'],
|
||||
['googleAuthConfig', 'fetchTransitiveGroupMembership'],
|
||||
['googleAuthConfig', 'allowedGroups'],
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="google-auth">
|
||||
<section className="header">
|
||||
<Typography.Text className="title">
|
||||
Edit Google Authentication
|
||||
</Typography.Text>
|
||||
<Typography.Paragraph className="description">
|
||||
<div className="authn-provider">
|
||||
<section className="authn-provider__header">
|
||||
<h3 className="authn-provider__title">Edit Google Authentication</h3>
|
||||
<p className="authn-provider__description">
|
||||
Enter OAuth 2.0 credentials obtained from the Google API Console below.
|
||||
Read the{' '}
|
||||
<a
|
||||
@@ -25,50 +72,247 @@ function ConfigureGoogleAuthAuthnProvider({
|
||||
docs
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</Typography.Paragraph>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<Form.Item
|
||||
label="Domain"
|
||||
name="name"
|
||||
className="field"
|
||||
tooltip={{
|
||||
title:
|
||||
'The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)',
|
||||
}}
|
||||
>
|
||||
<Input disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__columns">
|
||||
{/* Left Column - Core OAuth Settings */}
|
||||
<div className="authn-provider__left">
|
||||
<div className="authn-provider__field-group">
|
||||
<label className="authn-provider__label" htmlFor="google-domain">
|
||||
Domain
|
||||
<Tooltip title="The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name="name"
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{ required: true, message: 'Domain is required', whitespace: true },
|
||||
]}
|
||||
>
|
||||
<Input id="google-domain" disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="Client ID"
|
||||
name={['googleAuthConfig', 'clientId']}
|
||||
className="field"
|
||||
tooltip={{
|
||||
title: `ClientID is the application's ID. For example, 292085223830.apps.googleusercontent.com.`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__field-group">
|
||||
<label className="authn-provider__label" htmlFor="google-client-id">
|
||||
Client ID
|
||||
<Tooltip title="ClientID is the application's ID. For example, 292085223830.apps.googleusercontent.com.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'clientId']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{ required: true, message: 'Client ID is required', whitespace: true },
|
||||
]}
|
||||
>
|
||||
<Input id="google-client-id" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="Client Secret"
|
||||
name={['googleAuthConfig', 'clientSecret']}
|
||||
className="field"
|
||||
tooltip={{
|
||||
title: `It is the application's secret.`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__field-group">
|
||||
<label className="authn-provider__label" htmlFor="google-client-secret">
|
||||
Client Secret
|
||||
<Tooltip title="It is the application's secret.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'clientSecret']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Client Secret is required',
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input id="google-client-secret" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Callout
|
||||
type="warning"
|
||||
size="small"
|
||||
showIcon
|
||||
description="Google OAuth2 won’t be enabled unless you enter all the attributes above"
|
||||
className="callout"
|
||||
/>
|
||||
<div className="authn-provider__checkbox-row">
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'insecureSkipEmailVerified']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="google-skip-email-verification"
|
||||
labelName="Skip Email Verification"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
['googleAuthConfig', 'insecureSkipEmailVerified'],
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title='Whether to skip email verification. Defaults to "false"'>
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Callout
|
||||
type="warning"
|
||||
size="small"
|
||||
showIcon
|
||||
description="Google OAuth2 won't be enabled unless you enter all the attributes above"
|
||||
className="callout"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Google Workspace Groups (Advanced) */}
|
||||
<div className="authn-provider__right">
|
||||
<Collapse
|
||||
bordered={false}
|
||||
activeKey={
|
||||
expandedSection === 'workspace-groups' ? ['workspace-groups'] : []
|
||||
}
|
||||
onChange={handleWorkspaceGroupsChange}
|
||||
className="authn-provider__collapse"
|
||||
expandIcon={(): null => null}
|
||||
>
|
||||
<Collapse.Panel
|
||||
key="workspace-groups"
|
||||
header={
|
||||
<div className="authn-provider__collapse-header">
|
||||
{expandedSection !== 'workspace-groups' ? (
|
||||
<ChevronRight size={16} />
|
||||
) : (
|
||||
<ChevronDown size={16} />
|
||||
)}
|
||||
<div className="authn-provider__collapse-header-text">
|
||||
<h4 className="authn-provider__section-title">
|
||||
Google Workspace Groups (Advanced)
|
||||
</h4>
|
||||
<p className="authn-provider__section-description">
|
||||
Enable group fetching to retrieve user groups from Google Workspace.
|
||||
Requires a Service Account with domain-wide delegation.
|
||||
</p>
|
||||
</div>
|
||||
{expandedSection !== 'workspace-groups' && hasWorkspaceGroupsErrors && (
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
{workspaceGroupsErrorMessages.map((msg) => (
|
||||
<div key={msg}>{msg}</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TriangleAlert size={16} color={Color.BG_CHERRY_500} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="authn-provider__group-content">
|
||||
<div className="authn-provider__checkbox-row">
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'fetchGroups']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="google-fetch-groups"
|
||||
labelName="Fetch Groups"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(['googleAuthConfig', 'fetchGroups'], checked);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title="Enable fetching Google Workspace groups for the user. Requires service account configuration.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{fetchGroups && (
|
||||
<div className="authn-provider__group-fields">
|
||||
<div className="authn-provider__field-group">
|
||||
<label
|
||||
className="authn-provider__label"
|
||||
htmlFor="google-service-account-json"
|
||||
>
|
||||
Service Account JSON
|
||||
<Tooltip title="The JSON content of the Google Service Account credentials file. Required for group fetching.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'serviceAccountJson']}
|
||||
className="authn-provider__form-item"
|
||||
>
|
||||
<TextArea
|
||||
id="google-service-account-json"
|
||||
rows={3}
|
||||
placeholder="Paste service account JSON"
|
||||
className="authn-provider__textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<DomainMappingList
|
||||
fieldNamePrefix={['googleAuthConfig', 'domainToAdminEmailList']}
|
||||
/>
|
||||
|
||||
<div className="authn-provider__checkbox-row">
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'fetchTransitiveGroupMembership']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="google-transitive-membership"
|
||||
labelName="Fetch Transitive Group Membership"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
['googleAuthConfig', 'fetchTransitiveGroupMembership'],
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title="If enabled, recursively fetch groups that contain other groups (transitive membership).">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="authn-provider__field-group">
|
||||
<label
|
||||
className="authn-provider__label"
|
||||
htmlFor="google-allowed-groups"
|
||||
>
|
||||
Allowed Groups
|
||||
<Tooltip title="Optional list of allowed groups. If configured, only users belonging to one of these groups will be allowed to login.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['googleAuthConfig', 'allowedGroups']}
|
||||
className="authn-provider__form-item"
|
||||
>
|
||||
<EmailTagInput placeholder="Type a group email and press Enter" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
|
||||
<RoleMappingSection
|
||||
fieldNamePrefix={['roleMapping']}
|
||||
isExpanded={expandedSection === 'role-mapping'}
|
||||
onExpandChange={handleRoleMappingChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,110 +1,211 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Callout } from '@signozhq/callout';
|
||||
import { Checkbox, Form, Input, Typography } from 'antd';
|
||||
import { Checkbox } from '@signozhq/checkbox';
|
||||
import { CircleHelp } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Form, Tooltip } from 'antd';
|
||||
|
||||
import ClaimMappingSection from './components/ClaimMappingSection';
|
||||
import RoleMappingSection from './components/RoleMappingSection';
|
||||
|
||||
import './Providers.styles.scss';
|
||||
|
||||
type ExpandedSection = 'claim-mapping' | 'role-mapping' | null;
|
||||
|
||||
function ConfigureOIDCAuthnProvider({
|
||||
isCreate,
|
||||
}: {
|
||||
isCreate: boolean;
|
||||
}): JSX.Element {
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
const [expandedSection, setExpandedSection] = useState<ExpandedSection>(null);
|
||||
|
||||
const handleClaimMappingChange = useCallback((expanded: boolean): void => {
|
||||
setExpandedSection(expanded ? 'claim-mapping' : null);
|
||||
}, []);
|
||||
|
||||
const handleRoleMappingChange = useCallback((expanded: boolean): void => {
|
||||
setExpandedSection(expanded ? 'role-mapping' : null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="saml">
|
||||
<section className="header">
|
||||
<Typography.Text className="title">
|
||||
Edit OIDC Authentication
|
||||
</Typography.Text>
|
||||
<div className="authn-provider">
|
||||
<section className="authn-provider__header">
|
||||
<h3 className="authn-provider__title">Edit OIDC Authentication</h3>
|
||||
<p className="authn-provider__description">
|
||||
Configure OpenID Connect Single Sign-On with your Identity Provider. Read
|
||||
the{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/sso-authentication"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
docs
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<Form.Item
|
||||
label="Domain"
|
||||
name="name"
|
||||
tooltip={{
|
||||
title:
|
||||
'The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)',
|
||||
}}
|
||||
>
|
||||
<Input disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__columns">
|
||||
{/* Left Column - Core OIDC Settings */}
|
||||
<div className="authn-provider__left">
|
||||
<div className="authn-provider__field-group">
|
||||
<label className="authn-provider__label" htmlFor="oidc-domain">
|
||||
Domain
|
||||
<Tooltip title="The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name="name"
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{ required: true, message: 'Domain is required', whitespace: true },
|
||||
]}
|
||||
>
|
||||
<Input id="oidc-domain" disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="Issuer URL"
|
||||
name={['oidcConfig', 'issuer']}
|
||||
tooltip={{
|
||||
title: `It is the URL identifier for the service. For example: "https://accounts.google.com" or "https://login.salesforce.com".`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__field-group">
|
||||
<label className="authn-provider__label" htmlFor="oidc-issuer">
|
||||
Issuer URL
|
||||
<Tooltip title='The URL identifier for the OIDC provider. For example: "https://accounts.google.com" or "https://login.salesforce.com".'>
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['oidcConfig', 'issuer']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{ required: true, message: 'Issuer URL is required', whitespace: true },
|
||||
]}
|
||||
>
|
||||
<Input id="oidc-issuer" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="Issuer Alias"
|
||||
name={['oidcConfig', 'issuerAlias']}
|
||||
tooltip={{
|
||||
title: `Some offspec providers like Azure, Oracle IDCS have oidc discovery url different from issuer url which causes issuerValidation to fail.
|
||||
This provides a way to override the Issuer url from the .well-known/openid-configuration issuer`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__field-group">
|
||||
<label className="authn-provider__label" htmlFor="oidc-issuer-alias">
|
||||
Issuer Alias
|
||||
<Tooltip title="Optional: Override the issuer URL from .well-known/openid-configuration for providers like Azure or Oracle IDCS.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['oidcConfig', 'issuerAlias']}
|
||||
className="authn-provider__form-item"
|
||||
>
|
||||
<Input id="oidc-issuer-alias" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="Client ID"
|
||||
name={['oidcConfig', 'clientId']}
|
||||
tooltip={{ title: `It is the application's ID.` }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__field-group">
|
||||
<label className="authn-provider__label" htmlFor="oidc-client-id">
|
||||
Client ID
|
||||
<Tooltip title="The application's client ID from your OIDC provider.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['oidcConfig', 'clientId']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{ required: true, message: 'Client ID is required', whitespace: true },
|
||||
]}
|
||||
>
|
||||
<Input id="oidc-client-id" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="Client Secret"
|
||||
name={['oidcConfig', 'clientSecret']}
|
||||
tooltip={{ title: `It is the application's secret.` }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__field-group">
|
||||
<label className="authn-provider__label" htmlFor="oidc-client-secret">
|
||||
Client Secret
|
||||
<Tooltip title="The application's client secret from your OIDC provider.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['oidcConfig', 'clientSecret']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Client Secret is required',
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input id="oidc-client-secret" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="Email Claim Mapping"
|
||||
name={['oidcConfig', 'claimMapping', 'email']}
|
||||
tooltip={{
|
||||
title: `Mapping of email claims to the corresponding email field in the token.`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__checkbox-row">
|
||||
<Form.Item
|
||||
name={['oidcConfig', 'insecureSkipEmailVerified']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="oidc-skip-email-verification"
|
||||
labelName="Skip Email Verification"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
['oidcConfig', 'insecureSkipEmailVerified'],
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title='Whether to skip email verification. Defaults to "false"'>
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="Skip Email Verification"
|
||||
name={['oidcConfig', 'insecureSkipEmailVerified']}
|
||||
valuePropName="checked"
|
||||
className="field"
|
||||
tooltip={{
|
||||
title: `Whether to skip email verification. Defaults to "false"`,
|
||||
}}
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__checkbox-row">
|
||||
<Form.Item
|
||||
name={['oidcConfig', 'getUserInfo']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="oidc-get-user-info"
|
||||
labelName="Get User Info"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(['oidcConfig', 'getUserInfo'], checked);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title="Use the userinfo endpoint to get additional claims. Useful when providers return thin ID tokens.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="Get User Info"
|
||||
name={['oidcConfig', 'getUserInfo']}
|
||||
valuePropName="checked"
|
||||
className="field"
|
||||
tooltip={{
|
||||
title: `Uses the userinfo endpoint to get additional claims for the token. This is especially useful where upstreams return "thin" id tokens`,
|
||||
}}
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
<Callout
|
||||
type="warning"
|
||||
size="small"
|
||||
showIcon
|
||||
description="OIDC won't be enabled unless you enter all the attributes above"
|
||||
className="callout"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Callout
|
||||
type="warning"
|
||||
size="small"
|
||||
showIcon
|
||||
description="OIDC won’t be enabled unless you enter all the attributes above"
|
||||
className="callout"
|
||||
/>
|
||||
{/* Right Column - Advanced Settings */}
|
||||
<div className="authn-provider__right">
|
||||
<ClaimMappingSection
|
||||
fieldNamePrefix={['oidcConfig', 'claimMapping']}
|
||||
isExpanded={expandedSection === 'claim-mapping'}
|
||||
onExpandChange={handleClaimMappingChange}
|
||||
/>
|
||||
|
||||
<RoleMappingSection
|
||||
fieldNamePrefix={['roleMapping']}
|
||||
isExpanded={expandedSection === 'role-mapping'}
|
||||
onExpandChange={handleRoleMappingChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,82 +1,190 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Callout } from '@signozhq/callout';
|
||||
import { Checkbox, Form, Input, Typography } from 'antd';
|
||||
import { Checkbox } from '@signozhq/checkbox';
|
||||
import { CircleHelp } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Form, Tooltip } from 'antd';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
|
||||
import AttributeMappingSection from './components/AttributeMappingSection';
|
||||
import RoleMappingSection from './components/RoleMappingSection';
|
||||
|
||||
import './Providers.styles.scss';
|
||||
|
||||
type ExpandedSection = 'attribute-mapping' | 'role-mapping' | null;
|
||||
|
||||
function ConfigureSAMLAuthnProvider({
|
||||
isCreate,
|
||||
}: {
|
||||
isCreate: boolean;
|
||||
}): JSX.Element {
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
const [expandedSection, setExpandedSection] = useState<ExpandedSection>(null);
|
||||
|
||||
const handleAttributeMappingChange = useCallback((expanded: boolean): void => {
|
||||
setExpandedSection(expanded ? 'attribute-mapping' : null);
|
||||
}, []);
|
||||
|
||||
const handleRoleMappingChange = useCallback((expanded: boolean): void => {
|
||||
setExpandedSection(expanded ? 'role-mapping' : null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="saml">
|
||||
<section className="header">
|
||||
<Typography.Text className="title">
|
||||
Edit SAML Authentication
|
||||
</Typography.Text>
|
||||
<div className="authn-provider">
|
||||
<section className="authn-provider__header">
|
||||
<h3 className="authn-provider__title">Edit SAML Authentication</h3>
|
||||
<p className="authn-provider__description">
|
||||
Configure SAML 2.0 Single Sign-On with your Identity Provider. Read the{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/sso-authentication"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
docs
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<Form.Item
|
||||
label="Domain"
|
||||
name="name"
|
||||
tooltip={{
|
||||
title:
|
||||
'The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)',
|
||||
}}
|
||||
>
|
||||
<Input disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__columns">
|
||||
{/* Left Column - Core SAML Settings */}
|
||||
<div className="authn-provider__left">
|
||||
<div className="authn-provider__field-group">
|
||||
<label className="authn-provider__label" htmlFor="saml-domain">
|
||||
Domain
|
||||
<Tooltip title="The email domain for users who should use SSO (e.g., `example.com` for users with `@example.com` emails)">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name="name"
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{ required: true, message: 'Domain is required', whitespace: true },
|
||||
]}
|
||||
>
|
||||
<Input id="saml-domain" disabled={!isCreate} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="SAML ACS URL"
|
||||
name={['samlConfig', 'samlIdp']}
|
||||
tooltip={{
|
||||
title: `The SSO endpoint of the SAML identity provider. It can typically be found in the SingleSignOnService element in the SAML metadata of the identity provider. Example: <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="{samlIdp}"/>`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__field-group">
|
||||
<label className="authn-provider__label" htmlFor="saml-acs-url">
|
||||
SAML ACS URL
|
||||
<Tooltip title="The SSO endpoint of the SAML identity provider. It can typically be found in the SingleSignOnService element in the SAML metadata of the identity provider.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['samlConfig', 'samlIdp']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'SAML ACS URL is required',
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input id="saml-acs-url" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="SAML Entity ID"
|
||||
name={['samlConfig', 'samlEntity']}
|
||||
tooltip={{
|
||||
title: `The entityID of the SAML identity provider. It can typically be found in the EntityID attribute of the EntityDescriptor element in the SAML metadata of the identity provider. Example: <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="{samlEntity}">`,
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__field-group">
|
||||
<label className="authn-provider__label" htmlFor="saml-entity-id">
|
||||
SAML Entity ID
|
||||
<Tooltip title="The entityID of the SAML identity provider. It can typically be found in the EntityID attribute of the EntityDescriptor element in the SAML metadata.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['samlConfig', 'samlEntity']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'SAML Entity ID is required',
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input id="saml-entity-id" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="SAML X.509 Certificate"
|
||||
name={['samlConfig', 'samlCert']}
|
||||
tooltip={{
|
||||
title: `The certificate of the SAML identity provider. It can typically be found in the X509Certificate element in the SAML metadata of the identity provider. Example: <ds:X509Certificate><ds:X509Certificate>{samlCert}</ds:X509Certificate></ds:X509Certificate>`,
|
||||
}}
|
||||
>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__field-group">
|
||||
<label className="authn-provider__label" htmlFor="saml-certificate">
|
||||
SAML X.509 Certificate
|
||||
<Tooltip title="The certificate of the SAML identity provider. It can typically be found in the X509Certificate element in the SAML metadata.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={['samlConfig', 'samlCert']}
|
||||
className="authn-provider__form-item"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'SAML Certificate is required',
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextArea
|
||||
id="saml-certificate"
|
||||
rows={3}
|
||||
placeholder="Paste X.509 certificate"
|
||||
className="authn-provider__textarea"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label="Skip Signing AuthN Requests"
|
||||
name={['samlConfig', 'insecureSkipAuthNRequestsSigned']}
|
||||
valuePropName="checked"
|
||||
className="field"
|
||||
tooltip={{
|
||||
title: `Whether to skip signing the SAML requests. It can typically be found in the WantAuthnRequestsSigned attribute of the IDPSSODescriptor element in the SAML metadata of the identity provider. Example: <md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||
For providers like jumpcloud, this should be set to true.Note: This is the reverse of WantAuthnRequestsSigned. If WantAuthnRequestsSigned is false, then InsecureSkipAuthNRequestsSigned should be true.`,
|
||||
}}
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
<div className="authn-provider__checkbox-row">
|
||||
<Form.Item
|
||||
name={['samlConfig', 'insecureSkipAuthNRequestsSigned']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="saml-skip-signing"
|
||||
labelName="Skip Signing AuthN Requests"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue(
|
||||
['samlConfig', 'insecureSkipAuthNRequestsSigned'],
|
||||
checked,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title="Whether to skip signing the SAML requests. For providers like JumpCloud, this should be enabled.">
|
||||
<CircleHelp size={14} className="authn-provider__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Callout
|
||||
type="warning"
|
||||
size="small"
|
||||
showIcon
|
||||
description="SAML won’t be enabled unless you enter all the attributes above"
|
||||
className="callout"
|
||||
/>
|
||||
<Callout
|
||||
type="warning"
|
||||
size="small"
|
||||
showIcon
|
||||
description="SAML won't be enabled unless you enter all the attributes above"
|
||||
className="callout"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Advanced Settings */}
|
||||
<div className="authn-provider__right">
|
||||
<AttributeMappingSection
|
||||
fieldNamePrefix={['samlConfig', 'attributeMapping']}
|
||||
isExpanded={expandedSection === 'attribute-mapping'}
|
||||
onExpandChange={handleAttributeMappingChange}
|
||||
/>
|
||||
|
||||
<RoleMappingSection
|
||||
fieldNamePrefix={['roleMapping']}
|
||||
isExpanded={expandedSection === 'role-mapping'}
|
||||
onExpandChange={handleRoleMappingChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,246 @@
|
||||
.google-auth {
|
||||
.authn-provider {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 12px !important;
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
&__title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin: 0;
|
||||
color: var(--l2-foreground);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
a {
|
||||
color: var(--accent-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__columns {
|
||||
display: grid;
|
||||
grid-template-columns: 0.9fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__right {
|
||||
border-left: 1px solid var(--l3-border);
|
||||
padding-left: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
&__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__label-icon {
|
||||
color: var(--l3-foreground);
|
||||
cursor: help;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
&__checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
height: 32px;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-bottom: 0px !important;
|
||||
&:hover {
|
||||
border-color: var(--l3-border) !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: auto;
|
||||
}
|
||||
&__textarea {
|
||||
min-height: 60px !important;
|
||||
max-height: 200px;
|
||||
resize: vertical;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground) !important;
|
||||
font-family: 'SF Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
font-family: Inter, sans-serif;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border) !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
button[role='checkbox'] {
|
||||
border: 1px solid var(--l2-foreground) !important;
|
||||
border-radius: 2px;
|
||||
|
||||
&[data-state='checked'] {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse {
|
||||
background: transparent !important;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 12px 0 0 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
color: var(--l3-foreground);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__section-description {
|
||||
margin: 0;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
&__group-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__group-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
|
||||
.authn-provider__field-group,
|
||||
.authn-provider__checkbox-row {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-foreground);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--l3-foreground) transparent;
|
||||
}
|
||||
|
||||
.callout {
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
.attribute-mapping-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__collapse {
|
||||
background: transparent !important;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 12px 0 0 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
color: var(--l3-foreground);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__section-description {
|
||||
margin: 0;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
|
||||
// Thin scrollbar
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-foreground);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--l3-foreground) transparent;
|
||||
}
|
||||
|
||||
&__field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__label-icon {
|
||||
color: var(--l3-foreground);
|
||||
cursor: help;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 32px;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border) !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleHelp,
|
||||
TriangleAlert,
|
||||
} from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Collapse, Form, Tooltip } from 'antd';
|
||||
|
||||
import { useCollapseSectionErrors } from './hooks/useCollapseSectionErrors';
|
||||
|
||||
import './AttributeMappingSection.styles.scss';
|
||||
|
||||
interface AttributeMappingSectionProps {
|
||||
/** The form field name prefix for the attribute mapping configuration */
|
||||
fieldNamePrefix: string[];
|
||||
/** Whether the section is expanded (controlled mode) */
|
||||
isExpanded?: boolean;
|
||||
/** Callback when expand/collapse is toggled */
|
||||
onExpandChange?: (expanded: boolean) => void;
|
||||
}
|
||||
|
||||
function AttributeMappingSection({
|
||||
fieldNamePrefix,
|
||||
isExpanded,
|
||||
onExpandChange,
|
||||
}: AttributeMappingSectionProps): JSX.Element {
|
||||
// Support both controlled and uncontrolled modes
|
||||
const [internalExpanded, setInternalExpanded] = useState(false);
|
||||
const isControlled = isExpanded !== undefined;
|
||||
const expanded = isControlled ? isExpanded : internalExpanded;
|
||||
|
||||
const handleCollapseChange = useCallback(
|
||||
(keys: string | string[]): void => {
|
||||
const newExpanded = Array.isArray(keys) ? keys.length > 0 : !!keys;
|
||||
if (isControlled && onExpandChange) {
|
||||
onExpandChange(newExpanded);
|
||||
} else {
|
||||
setInternalExpanded(newExpanded);
|
||||
}
|
||||
},
|
||||
[isControlled, onExpandChange],
|
||||
);
|
||||
|
||||
const collapseActiveKey = expanded ? ['attribute-mapping'] : [];
|
||||
const { hasErrors, errorMessages } = useCollapseSectionErrors(fieldNamePrefix);
|
||||
|
||||
return (
|
||||
<div className="attribute-mapping-section">
|
||||
<Collapse
|
||||
bordered={false}
|
||||
activeKey={collapseActiveKey}
|
||||
onChange={handleCollapseChange}
|
||||
className="attribute-mapping-section__collapse"
|
||||
expandIcon={(): null => null}
|
||||
>
|
||||
<Collapse.Panel
|
||||
key="attribute-mapping"
|
||||
header={
|
||||
<div
|
||||
className="attribute-mapping-section__collapse-header"
|
||||
role="button"
|
||||
aria-expanded={expanded}
|
||||
aria-controls="attribute-mapping-content"
|
||||
>
|
||||
{!expanded ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
||||
<div className="attribute-mapping-section__collapse-header-text">
|
||||
<h4 className="attribute-mapping-section__section-title">
|
||||
Attribute Mapping (Advanced)
|
||||
</h4>
|
||||
<p className="attribute-mapping-section__section-description">
|
||||
Configure how SAML assertion attributes from your Identity Provider map
|
||||
to SigNoz user attributes. Leave empty to use default values.
|
||||
</p>
|
||||
</div>
|
||||
{!expanded && hasErrors && (
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
{errorMessages.map((msg) => (
|
||||
<div key={msg}>{msg}</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TriangleAlert size={16} color={Color.BG_CHERRY_500} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
id="attribute-mapping-content"
|
||||
className="attribute-mapping-section__content"
|
||||
>
|
||||
<div className="attribute-mapping-section__field-group">
|
||||
<label
|
||||
className="attribute-mapping-section__label"
|
||||
htmlFor="email-attribute"
|
||||
>
|
||||
Email Attribute
|
||||
<Tooltip title="The SAML attribute key that contains the user's email. Default: 'email'">
|
||||
<CircleHelp
|
||||
size={14}
|
||||
className="attribute-mapping-section__label-icon"
|
||||
/>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'email']}
|
||||
className="attribute-mapping-section__form-item"
|
||||
>
|
||||
<Input id="email-attribute" placeholder="Email" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Name Attribute */}
|
||||
<div className="attribute-mapping-section__field-group">
|
||||
<label
|
||||
className="attribute-mapping-section__label"
|
||||
htmlFor="name-attribute"
|
||||
>
|
||||
Name Attribute
|
||||
<Tooltip title="The SAML attribute key that contains the user's display name. Default: 'name'">
|
||||
<CircleHelp
|
||||
size={14}
|
||||
className="attribute-mapping-section__label-icon"
|
||||
/>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'name']}
|
||||
className="attribute-mapping-section__form-item"
|
||||
>
|
||||
<Input id="name-attribute" placeholder="Name" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Groups Attribute */}
|
||||
<div className="attribute-mapping-section__field-group">
|
||||
<label
|
||||
className="attribute-mapping-section__label"
|
||||
htmlFor="groups-attribute"
|
||||
>
|
||||
Groups Attribute
|
||||
<Tooltip title="The SAML attribute key that contains the user's group memberships. Used for role mapping. Default: 'groups'">
|
||||
<CircleHelp
|
||||
size={14}
|
||||
className="attribute-mapping-section__label-icon"
|
||||
/>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'groups']}
|
||||
className="attribute-mapping-section__form-item"
|
||||
>
|
||||
<Input id="groups-attribute" placeholder="Groups" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Role Attribute */}
|
||||
<div className="attribute-mapping-section__field-group">
|
||||
<label
|
||||
className="attribute-mapping-section__label"
|
||||
htmlFor="role-attribute"
|
||||
>
|
||||
Role Attribute
|
||||
<Tooltip title="The SAML attribute key that contains the user's role directly from the IDP. Default: 'role'">
|
||||
<CircleHelp
|
||||
size={14}
|
||||
className="attribute-mapping-section__label-icon"
|
||||
/>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'role']}
|
||||
className="attribute-mapping-section__form-item"
|
||||
>
|
||||
<Input id="role-attribute" placeholder="Role" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AttributeMappingSection;
|
||||
@@ -0,0 +1,133 @@
|
||||
.claim-mapping-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__collapse {
|
||||
background: transparent !important;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 12px 0 0 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
color: var(--l3-foreground);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__section-description {
|
||||
margin: 0;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
|
||||
// Thin scrollbar
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-foreground);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--l3-foreground) transparent;
|
||||
}
|
||||
|
||||
&__field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__label-icon {
|
||||
color: var(--l3-foreground);
|
||||
cursor: help;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 32px;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border) !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleHelp,
|
||||
TriangleAlert,
|
||||
} from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Collapse, Form, Tooltip } from 'antd';
|
||||
|
||||
import { useCollapseSectionErrors } from './hooks/useCollapseSectionErrors';
|
||||
|
||||
import './ClaimMappingSection.styles.scss';
|
||||
|
||||
interface ClaimMappingSectionProps {
|
||||
/** The form field name prefix for the claim mapping configuration */
|
||||
fieldNamePrefix: string[];
|
||||
/** Whether the section is expanded (controlled mode) */
|
||||
isExpanded?: boolean;
|
||||
/** Callback when expand/collapse is toggled */
|
||||
onExpandChange?: (expanded: boolean) => void;
|
||||
}
|
||||
|
||||
function ClaimMappingSection({
|
||||
fieldNamePrefix,
|
||||
isExpanded,
|
||||
onExpandChange,
|
||||
}: ClaimMappingSectionProps): JSX.Element {
|
||||
// Support both controlled and uncontrolled modes
|
||||
const [internalExpanded, setInternalExpanded] = useState(false);
|
||||
const isControlled = isExpanded !== undefined;
|
||||
const expanded = isControlled ? isExpanded : internalExpanded;
|
||||
|
||||
const handleCollapseChange = useCallback(
|
||||
(keys: string | string[]): void => {
|
||||
const newExpanded = Array.isArray(keys) ? keys.length > 0 : !!keys;
|
||||
if (isControlled && onExpandChange) {
|
||||
onExpandChange(newExpanded);
|
||||
} else {
|
||||
setInternalExpanded(newExpanded);
|
||||
}
|
||||
},
|
||||
[isControlled, onExpandChange],
|
||||
);
|
||||
|
||||
const collapseActiveKey = expanded ? ['claim-mapping'] : [];
|
||||
const { hasErrors, errorMessages } = useCollapseSectionErrors(fieldNamePrefix);
|
||||
|
||||
return (
|
||||
<div className="claim-mapping-section">
|
||||
<Collapse
|
||||
bordered={false}
|
||||
activeKey={collapseActiveKey}
|
||||
onChange={handleCollapseChange}
|
||||
className="claim-mapping-section__collapse"
|
||||
expandIcon={(): null => null}
|
||||
>
|
||||
<Collapse.Panel
|
||||
key="claim-mapping"
|
||||
header={
|
||||
<div
|
||||
className="claim-mapping-section__collapse-header"
|
||||
role="button"
|
||||
aria-expanded={expanded}
|
||||
aria-controls="claim-mapping-content"
|
||||
>
|
||||
{!expanded ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
||||
<div className="claim-mapping-section__collapse-header-text">
|
||||
<h4 className="claim-mapping-section__section-title">
|
||||
Claim Mapping (Advanced)
|
||||
</h4>
|
||||
<p className="claim-mapping-section__section-description">
|
||||
Configure how claims from your Identity Provider map to SigNoz user
|
||||
attributes. Leave empty to use default values.
|
||||
</p>
|
||||
</div>
|
||||
{!expanded && hasErrors && (
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
{errorMessages.map((msg) => (
|
||||
<div key={msg}>{msg}</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TriangleAlert size={16} color={Color.BG_CHERRY_500} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div id="claim-mapping-content" className="claim-mapping-section__content">
|
||||
{/* Email Claim */}
|
||||
<div className="claim-mapping-section__field-group">
|
||||
<label className="claim-mapping-section__label" htmlFor="email-claim">
|
||||
Email Claim
|
||||
<Tooltip title="The claim key that contains the user's email address. Default: 'email'">
|
||||
<CircleHelp size={14} className="claim-mapping-section__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'email']}
|
||||
className="claim-mapping-section__form-item"
|
||||
>
|
||||
<Input id="email-claim" placeholder="Email" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Name Claim */}
|
||||
<div className="claim-mapping-section__field-group">
|
||||
<label className="claim-mapping-section__label" htmlFor="name-claim">
|
||||
Name Claim
|
||||
<Tooltip title="The claim key that contains the user's display name. Default: 'name'">
|
||||
<CircleHelp size={14} className="claim-mapping-section__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'name']}
|
||||
className="claim-mapping-section__form-item"
|
||||
>
|
||||
<Input id="name-claim" placeholder="Name" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Groups Claim */}
|
||||
<div className="claim-mapping-section__field-group">
|
||||
<label className="claim-mapping-section__label" htmlFor="groups-claim">
|
||||
Groups Claim
|
||||
<Tooltip title="The claim key that contains the user's group memberships. Used for role mapping. Default: 'groups'">
|
||||
<CircleHelp size={14} className="claim-mapping-section__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'groups']}
|
||||
className="claim-mapping-section__form-item"
|
||||
>
|
||||
<Input id="groups-claim" placeholder="Groups" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Role Claim */}
|
||||
<div className="claim-mapping-section__field-group">
|
||||
<label className="claim-mapping-section__label" htmlFor="role-claim">
|
||||
Role Claim
|
||||
<Tooltip title="The claim key that contains the user's role directly from the IDP. Default: 'role'">
|
||||
<CircleHelp size={14} className="claim-mapping-section__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'role']}
|
||||
className="claim-mapping-section__form-item"
|
||||
>
|
||||
<Input id="role-claim" placeholder="Role" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClaimMappingSection;
|
||||
@@ -0,0 +1,103 @@
|
||||
.domain-mapping-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin: 0;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
&__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__remove-btn {
|
||||
flex-shrink: 0;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
min-width: 32px !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 2px !important;
|
||||
background: transparent !important;
|
||||
color: var(--bg-cherry-500) !important;
|
||||
opacity: 0.6 !important;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, opacity 0.2s;
|
||||
box-shadow: none !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
color: var(--bg-cherry-500) !important;
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(229, 72, 77, 0.1) !important;
|
||||
opacity: 0.9 !important;
|
||||
color: var(--bg-cherry-500) !important;
|
||||
|
||||
svg {
|
||||
color: var(--bg-cherry-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7 !important;
|
||||
background: rgba(229, 72, 77, 0.15) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-btn {
|
||||
width: 100%;
|
||||
|
||||
// Ensure icon is visible
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l2-foreground) !important;
|
||||
display: inline-block !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Plus, Trash2 } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Form } from 'antd';
|
||||
|
||||
import './DomainMappingList.styles.scss';
|
||||
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
const validateEmail = (_: unknown, value: string): Promise<void> => {
|
||||
if (!value) {
|
||||
return Promise.reject(new Error('Admin email is required'));
|
||||
}
|
||||
if (!EMAIL_REGEX.test(value)) {
|
||||
return Promise.reject(new Error('Please enter a valid email'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
interface DomainMappingListProps {
|
||||
/** The form field name prefix for the domain mapping list */
|
||||
fieldNamePrefix: string[];
|
||||
}
|
||||
|
||||
function DomainMappingList({
|
||||
fieldNamePrefix,
|
||||
}: DomainMappingListProps): JSX.Element {
|
||||
return (
|
||||
<div className="domain-mapping-list">
|
||||
<div className="domain-mapping-list__header">
|
||||
<span className="domain-mapping-list__title">
|
||||
Domain to Admin Email Mapping
|
||||
</span>
|
||||
<p className="domain-mapping-list__description">
|
||||
Map workspace domains to admin emails for service account impersonation.
|
||||
Use "*" as a wildcard for any domain.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form.List name={fieldNamePrefix}>
|
||||
{(fields, { add, remove }): JSX.Element => (
|
||||
<div className="domain-mapping-list__items">
|
||||
{fields.map((field) => (
|
||||
<div key={field.key} className="domain-mapping-list__row">
|
||||
<Form.Item
|
||||
name={[field.name, 'domain']}
|
||||
className="domain-mapping-list__field"
|
||||
rules={[{ required: true, message: 'Domain is required' }]}
|
||||
>
|
||||
<Input placeholder="Domain (e.g., example.com or *)" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[field.name, 'adminEmail']}
|
||||
className="domain-mapping-list__field"
|
||||
rules={[{ validator: validateEmail }]}
|
||||
>
|
||||
<Input placeholder="Admin Email" />
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className="domain-mapping-list__remove-btn"
|
||||
onClick={(): void => remove(field.name)}
|
||||
aria-label="Remove mapping"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="dashed"
|
||||
onClick={(): void => add({ domain: '', adminEmail: '' })}
|
||||
prefixIcon={<Plus size={14} />}
|
||||
className="domain-mapping-list__add-btn"
|
||||
>
|
||||
Add Domain Mapping
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DomainMappingList;
|
||||
@@ -0,0 +1,28 @@
|
||||
.email-tag-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&__select {
|
||||
width: 100%;
|
||||
|
||||
.ant-select-selector {
|
||||
.ant-select-selection-search {
|
||||
input {
|
||||
height: 32px !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
box-shadow: none !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__error {
|
||||
margin: 0;
|
||||
color: var(--bg-cherry-500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Select, Tooltip } from 'antd';
|
||||
|
||||
import './EmailTagInput.styles.scss';
|
||||
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
interface EmailTagInputProps {
|
||||
/** Current value (array of email strings) */
|
||||
value?: string[];
|
||||
/** Change handler */
|
||||
onChange?: (value: string[]) => void;
|
||||
/** Placeholder text */
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
function EmailTagInput({
|
||||
value = [],
|
||||
onChange,
|
||||
placeholder = 'Type an email and press Enter',
|
||||
}: EmailTagInputProps): JSX.Element {
|
||||
const [validationError, setValidationError] = useState('');
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValues: string[]): void => {
|
||||
const addedValues = newValues.filter((v) => !value.includes(v));
|
||||
const invalidEmail = addedValues.find((v) => !EMAIL_REGEX.test(v));
|
||||
|
||||
if (invalidEmail) {
|
||||
setValidationError(`"${invalidEmail}" is not a valid email`);
|
||||
return;
|
||||
}
|
||||
setValidationError('');
|
||||
onChange?.(newValues);
|
||||
},
|
||||
[onChange, value],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="email-tag-input">
|
||||
<Tooltip
|
||||
title={validationError}
|
||||
open={!!validationError}
|
||||
placement="topRight"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
tokenSeparators={[',', ' ']}
|
||||
className="email-tag-input__select"
|
||||
allowClear
|
||||
status={validationError ? 'error' : undefined}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailTagInput;
|
||||
@@ -0,0 +1,294 @@
|
||||
.role-mapping-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 24px;
|
||||
|
||||
&__collapse {
|
||||
background: transparent !important;
|
||||
|
||||
.ant-collapse-item {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 12px 0 0 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
color: var(--l3-foreground);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__collapse-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__section-description {
|
||||
margin: 0;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: 45vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
|
||||
// Thin scrollbar
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-foreground);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--l3-foreground) transparent;
|
||||
}
|
||||
|
||||
&__field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__label-icon {
|
||||
color: var(--l3-foreground);
|
||||
cursor: help;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
&__checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__select {
|
||||
width: 100%;
|
||||
|
||||
&.ant-select {
|
||||
.ant-select-selector {
|
||||
height: 32px;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .ant-select-selector {
|
||||
border-color: var(--l3-border) !important;
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__group-mappings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__group-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__group-title {
|
||||
margin: 0;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__group-description {
|
||||
margin: 0;
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
&__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__field {
|
||||
&--group {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
&--role {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
&__remove-btn {
|
||||
flex-shrink: 0;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
min-width: 32px !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 2px !important;
|
||||
background: transparent !important;
|
||||
color: var(--bg-cherry-500) !important;
|
||||
opacity: 0.6 !important;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, opacity 0.2s;
|
||||
box-shadow: none !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
color: var(--bg-cherry-500) !important;
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(229, 72, 77, 0.1) !important;
|
||||
opacity: 0.9 !important;
|
||||
color: var(--bg-cherry-500) !important;
|
||||
|
||||
svg {
|
||||
color: var(--bg-cherry-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7 !important;
|
||||
background: rgba(229, 72, 77, 0.15) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-btn {
|
||||
width: 100%;
|
||||
|
||||
// Ensure icon is visible
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l2-foreground) !important;
|
||||
display: inline-block !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Checkbox border visibility ---
|
||||
button[role='checkbox'] {
|
||||
border: 1px solid var(--l2-foreground) !important;
|
||||
border-radius: 2px;
|
||||
|
||||
&[data-state='checked'] {
|
||||
background-color: var(--bg-robin-500) !important;
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Input styles ---
|
||||
input {
|
||||
height: 32px;
|
||||
background: var(--l3-background) !important;
|
||||
border: 1px solid var(--l3-border) !important;
|
||||
border-radius: 2px;
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l3-border) !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Checkbox } from '@signozhq/checkbox';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleHelp,
|
||||
Plus,
|
||||
Trash2,
|
||||
TriangleAlert,
|
||||
} from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/input';
|
||||
import { Collapse, Form, Select, Tooltip } from 'antd';
|
||||
|
||||
import { useCollapseSectionErrors } from './hooks/useCollapseSectionErrors';
|
||||
|
||||
import './RoleMappingSection.styles.scss';
|
||||
|
||||
const ROLE_OPTIONS = [
|
||||
{ value: 'VIEWER', label: 'VIEWER' },
|
||||
{ value: 'EDITOR', label: 'EDITOR' },
|
||||
{ value: 'ADMIN', label: 'ADMIN' },
|
||||
];
|
||||
|
||||
interface RoleMappingSectionProps {
|
||||
/** The form field name prefix for the role mapping configuration */
|
||||
fieldNamePrefix: string[];
|
||||
/** Whether the section is expanded (controlled mode) */
|
||||
isExpanded?: boolean;
|
||||
/** Callback when expand/collapse is toggled */
|
||||
onExpandChange?: (expanded: boolean) => void;
|
||||
}
|
||||
|
||||
function RoleMappingSection({
|
||||
fieldNamePrefix,
|
||||
isExpanded,
|
||||
onExpandChange,
|
||||
}: RoleMappingSectionProps): JSX.Element {
|
||||
const form = Form.useFormInstance();
|
||||
const useRoleAttribute = Form.useWatch(
|
||||
[...fieldNamePrefix, 'useRoleAttribute'],
|
||||
form,
|
||||
);
|
||||
|
||||
// Support both controlled and uncontrolled modes
|
||||
const [internalExpanded, setInternalExpanded] = useState(false);
|
||||
const isControlled = isExpanded !== undefined;
|
||||
const expanded = isControlled ? isExpanded : internalExpanded;
|
||||
|
||||
const handleCollapseChange = useCallback(
|
||||
(keys: string | string[]): void => {
|
||||
const newExpanded = Array.isArray(keys) ? keys.length > 0 : !!keys;
|
||||
if (isControlled && onExpandChange) {
|
||||
onExpandChange(newExpanded);
|
||||
} else {
|
||||
setInternalExpanded(newExpanded);
|
||||
}
|
||||
},
|
||||
[isControlled, onExpandChange],
|
||||
);
|
||||
|
||||
const collapseActiveKey = expanded ? ['role-mapping'] : [];
|
||||
const { hasErrors, errorMessages } = useCollapseSectionErrors(fieldNamePrefix);
|
||||
|
||||
return (
|
||||
<div className="role-mapping-section">
|
||||
<Collapse
|
||||
bordered={false}
|
||||
activeKey={collapseActiveKey}
|
||||
onChange={handleCollapseChange}
|
||||
className="role-mapping-section__collapse"
|
||||
expandIcon={(): null => null}
|
||||
>
|
||||
<Collapse.Panel
|
||||
key="role-mapping"
|
||||
header={
|
||||
<div
|
||||
className="role-mapping-section__collapse-header"
|
||||
role="button"
|
||||
aria-expanded={expanded}
|
||||
aria-controls="role-mapping-content"
|
||||
>
|
||||
{!expanded ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
||||
<div className="role-mapping-section__collapse-header-text">
|
||||
<h4 className="role-mapping-section__section-title">
|
||||
Role Mapping (Advanced)
|
||||
</h4>
|
||||
<p className="role-mapping-section__section-description">
|
||||
Configure how user roles are determined from your Identity Provider.
|
||||
You can either use a direct role attribute or map IDP groups to SigNoz
|
||||
roles.
|
||||
</p>
|
||||
</div>
|
||||
{!expanded && hasErrors && (
|
||||
<Tooltip
|
||||
title={
|
||||
<div>
|
||||
{errorMessages.map((msg) => (
|
||||
<div key={msg}>{msg}</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<TriangleAlert size={16} color={Color.BG_CHERRY_500} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div id="role-mapping-content" className="role-mapping-section__content">
|
||||
{/* Default Role */}
|
||||
<div className="role-mapping-section__field-group">
|
||||
<label className="role-mapping-section__label" htmlFor="default-role">
|
||||
Default Role
|
||||
<Tooltip title='The default role assigned to new SSO users if no other role mapping applies. Default: "VIEWER"'>
|
||||
<CircleHelp size={14} className="role-mapping-section__label-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'defaultRole']}
|
||||
className="role-mapping-section__form-item"
|
||||
initialValue="VIEWER"
|
||||
>
|
||||
<Select
|
||||
id="default-role"
|
||||
options={ROLE_OPTIONS}
|
||||
className="role-mapping-section__select"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Use Role Attribute */}
|
||||
<div className="role-mapping-section__checkbox-row">
|
||||
<Form.Item
|
||||
name={[...fieldNamePrefix, 'useRoleAttribute']}
|
||||
valuePropName="checked"
|
||||
noStyle
|
||||
>
|
||||
<Checkbox
|
||||
id="use-role-attribute"
|
||||
labelName="Use Role Attribute Directly"
|
||||
onCheckedChange={(checked: boolean): void => {
|
||||
form.setFieldValue([...fieldNamePrefix, 'useRoleAttribute'], checked);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title="If enabled, the role claim/attribute from the IDP will be used directly instead of group mappings. The role value must match a SigNoz role (VIEWER, EDITOR, or ADMIN).">
|
||||
<CircleHelp size={14} className="role-mapping-section__label-icon" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Group to Role Mappings - only show when useRoleAttribute is false */}
|
||||
{!useRoleAttribute && (
|
||||
<div className="role-mapping-section__group-mappings">
|
||||
<div className="role-mapping-section__group-header">
|
||||
<span className="role-mapping-section__group-title">
|
||||
Group to Role Mappings
|
||||
</span>
|
||||
<p className="role-mapping-section__group-description">
|
||||
Map IDP group names to SigNoz roles. If a user belongs to multiple
|
||||
groups, the highest privilege role will be assigned.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form.List name={[...fieldNamePrefix, 'groupMappingsList']}>
|
||||
{(fields, { add, remove }): JSX.Element => (
|
||||
<div className="role-mapping-section__items">
|
||||
{fields.map((field) => (
|
||||
<div key={field.key} className="role-mapping-section__row">
|
||||
<Form.Item
|
||||
name={[field.name, 'groupName']}
|
||||
className="role-mapping-section__field role-mapping-section__field--group"
|
||||
rules={[{ required: true, message: 'Group name is required' }]}
|
||||
>
|
||||
<Input placeholder="IDP Group Name" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={[field.name, 'role']}
|
||||
className="role-mapping-section__field role-mapping-section__field--role"
|
||||
rules={[{ required: true, message: 'Role is required' }]}
|
||||
initialValue="VIEWER"
|
||||
>
|
||||
<Select
|
||||
options={ROLE_OPTIONS}
|
||||
className="role-mapping-section__select"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
className="role-mapping-section__remove-btn"
|
||||
onClick={(): void => remove(field.name)}
|
||||
aria-label="Remove mapping"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="dashed"
|
||||
onClick={(): void => add({ groupName: '', role: 'VIEWER' })}
|
||||
prefixIcon={<Plus size={14} />}
|
||||
className="role-mapping-section__add-btn"
|
||||
>
|
||||
Add Group Mapping
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RoleMappingSection;
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Form } from 'antd';
|
||||
|
||||
export interface CollapseSectionErrors {
|
||||
hasErrors: boolean;
|
||||
errorMessages: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects validation errors in a form section
|
||||
* @param fieldNamePrefix - Field path prefix for the section
|
||||
* @param specificFields - Optional specific field prefixes to check (uses prefix matching)
|
||||
*/
|
||||
export function useCollapseSectionErrors(
|
||||
fieldNamePrefix: string[],
|
||||
specificFields?: string[][],
|
||||
): CollapseSectionErrors {
|
||||
const form = Form.useFormInstance();
|
||||
Form.useWatch([], form);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
return useMemo(() => {
|
||||
const fieldErrors = form.getFieldsError();
|
||||
const messages: string[] = [];
|
||||
|
||||
if (specificFields?.length) {
|
||||
fieldErrors.forEach((field) => {
|
||||
const fieldPath = Array.isArray(field.name) ? field.name : [field.name];
|
||||
|
||||
const isMatch = specificFields.some((specificField) => {
|
||||
if (fieldPath.length < specificField.length) {
|
||||
return false;
|
||||
}
|
||||
return specificField.every((part, idx) => fieldPath[idx] === part);
|
||||
});
|
||||
|
||||
if (isMatch && field.errors.length > 0) {
|
||||
field.errors.forEach((error) => messages.push(error));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const prefixPath = fieldNamePrefix.join('.');
|
||||
|
||||
fieldErrors.forEach((field) => {
|
||||
const fieldPath = Array.isArray(field.name)
|
||||
? field.name.join('.')
|
||||
: String(field.name);
|
||||
|
||||
if (fieldPath.startsWith(prefixPath) && field.errors.length > 0) {
|
||||
field.errors.forEach((error) => messages.push(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
hasErrors: messages.length > 0,
|
||||
errorMessages: messages,
|
||||
};
|
||||
}, [form, fieldNamePrefix, specificFields]);
|
||||
}
|
||||
@@ -1,45 +1,74 @@
|
||||
import { useState } from 'react';
|
||||
import { Switch } from 'antd';
|
||||
import put from 'api/v1/domains/id/put';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Switch } from '@signozhq/switch';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { useUpdateAuthDomain } from 'api/generated/services/authdomains';
|
||||
import {
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { GettableAuthDomain } from 'types/api/v1/domains/list';
|
||||
|
||||
function Toggle({ isDefaultChecked, record }: ToggleProps): JSX.Element {
|
||||
const [isChecked, setIsChecked] = useState<boolean>(isDefaultChecked);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const onChangeHandler = async (checked: boolean): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await put({
|
||||
id: record.id,
|
||||
config: {
|
||||
ssoEnabled: checked,
|
||||
ssoType: record.ssoType,
|
||||
googleAuthConfig: record.googleAuthConfig,
|
||||
oidcConfig: record.oidcConfig,
|
||||
samlConfig: record.samlConfig,
|
||||
},
|
||||
});
|
||||
setIsChecked(checked);
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch loading={isLoading} checked={isChecked} onChange={onChangeHandler} />
|
||||
);
|
||||
}
|
||||
|
||||
interface ToggleProps {
|
||||
isDefaultChecked: boolean;
|
||||
record: GettableAuthDomain;
|
||||
record: AuthtypesGettableAuthDomainDTO;
|
||||
}
|
||||
|
||||
function Toggle({ isDefaultChecked, record }: ToggleProps): JSX.Element {
|
||||
const [isChecked, setIsChecked] = useState<boolean>(isDefaultChecked);
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
useEffect(() => {
|
||||
setIsChecked(isDefaultChecked);
|
||||
}, [isDefaultChecked]);
|
||||
|
||||
const { mutate: updateAuthDomain, isLoading } = useUpdateAuthDomain<
|
||||
AxiosError<RenderErrorResponseDTO>
|
||||
>();
|
||||
|
||||
const onChangeHandler = (checked: boolean): void => {
|
||||
if (!record.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsChecked(checked);
|
||||
|
||||
updateAuthDomain(
|
||||
{
|
||||
pathParams: { id: record.id },
|
||||
data: {
|
||||
config: {
|
||||
ssoEnabled: checked,
|
||||
ssoType: record.ssoType,
|
||||
googleAuthConfig: record.googleAuthConfig,
|
||||
oidcConfig: record.oidcConfig,
|
||||
samlConfig: record.samlConfig,
|
||||
roleMapping: record.roleMapping,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
setIsChecked(!checked);
|
||||
try {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
} catch (apiError) {
|
||||
showErrorModal(apiError as APIError);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
disabled={isLoading}
|
||||
checked={isChecked}
|
||||
onCheckedChange={onChangeHandler}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Toggle;
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import AuthDomain from '../index';
|
||||
import {
|
||||
AUTH_DOMAINS_LIST_ENDPOINT,
|
||||
mockDomainsListResponse,
|
||||
mockEmptyDomainsResponse,
|
||||
mockErrorResponse,
|
||||
} from './mocks';
|
||||
|
||||
const successNotification = jest.fn();
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
__esModule: true,
|
||||
useNotifications: jest.fn(() => ({
|
||||
notifications: {
|
||||
success: successNotification,
|
||||
error: jest.fn(),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('AuthDomain', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('List View', () => {
|
||||
it('renders page header and add button', async () => {
|
||||
server.use(
|
||||
rest.get(AUTH_DOMAINS_LIST_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockEmptyDomainsResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<AuthDomain />);
|
||||
|
||||
expect(screen.getByText(/authenticated domains/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /add domain/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders list of auth domains successfully', async () => {
|
||||
server.use(
|
||||
rest.get(AUTH_DOMAINS_LIST_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockDomainsListResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<AuthDomain />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('signoz.io')).toBeInTheDocument();
|
||||
expect(screen.getByText('example.com')).toBeInTheDocument();
|
||||
expect(screen.getByText('corp.io')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders empty state when no domains exist', async () => {
|
||||
server.use(
|
||||
rest.get(AUTH_DOMAINS_LIST_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockEmptyDomainsResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<AuthDomain />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/no data/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays error content when API fails', async () => {
|
||||
server.use(
|
||||
rest.get(AUTH_DOMAINS_LIST_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(500), ctx.json(mockErrorResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<AuthDomain />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/failed to perform operation/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add Domain', () => {
|
||||
it('opens create modal when Add Domain button is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.get(AUTH_DOMAINS_LIST_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockEmptyDomainsResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<AuthDomain />);
|
||||
|
||||
const addButton = await screen.findByRole('button', { name: /add domain/i });
|
||||
await user.click(addButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/configure authentication method/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configure Domain', () => {
|
||||
it('opens edit modal when configure action is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.get(AUTH_DOMAINS_LIST_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockDomainsListResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<AuthDomain />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('signoz.io')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const configureLinks = await screen.findAllByText(/configure google auth/i);
|
||||
await user.click(configureLinks[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/edit google authentication/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,354 @@
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import CreateEdit from '../CreateEdit/CreateEdit';
|
||||
import {
|
||||
mockDomainWithRoleMapping,
|
||||
mockGoogleAuthDomain,
|
||||
mockGoogleAuthWithWorkspaceGroups,
|
||||
mockOidcAuthDomain,
|
||||
mockOidcWithClaimMapping,
|
||||
mockSamlAuthDomain,
|
||||
mockSamlWithAttributeMapping,
|
||||
} from './mocks';
|
||||
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
describe('CreateEdit Modal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Provider Selection (Create Mode)', () => {
|
||||
it('renders provider selection when creating new domain', () => {
|
||||
render(<CreateEdit isCreate onClose={mockOnClose} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(/configure authentication method/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/google apps authentication/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/saml authentication/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/oidc authentication/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('returns to provider selection when back button is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<CreateEdit isCreate onClose={mockOnClose} />);
|
||||
|
||||
const configureButtons = await screen.findAllByRole('button', {
|
||||
name: /configure/i,
|
||||
});
|
||||
await user.click(configureButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/edit google authentication/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const backButton = screen.getByRole('button', { name: /back/i });
|
||||
await user.click(backButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/configure authentication method/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit Mode', () => {
|
||||
it('shows provider form directly when editing existing domain', () => {
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockGoogleAuthDomain}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/edit google authentication/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/configure authentication method/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pre-fills form with existing domain values', () => {
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockGoogleAuthDomain}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByDisplayValue('signoz.io')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('test-client-id')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables domain field when editing', () => {
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockGoogleAuthDomain}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
const domainInput = screen.getByDisplayValue('signoz.io');
|
||||
expect(domainInput).toBeDisabled();
|
||||
});
|
||||
|
||||
it('shows cancel button instead of back when editing', () => {
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockGoogleAuthDomain}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /back/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form Validation', () => {
|
||||
it('shows validation error when submitting without required fields', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<CreateEdit isCreate onClose={mockOnClose} />);
|
||||
|
||||
const configureButtons = await screen.findAllByRole('button', {
|
||||
name: /configure/i,
|
||||
});
|
||||
await user.click(configureButtons[0]);
|
||||
|
||||
const saveButton = await screen.findByRole('button', {
|
||||
name: /save changes/i,
|
||||
});
|
||||
await user.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/domain is required/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Google Auth Provider', () => {
|
||||
it('shows Google Auth form fields', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<CreateEdit isCreate onClose={mockOnClose} />);
|
||||
|
||||
const configureButtons = await screen.findAllByRole('button', {
|
||||
name: /configure/i,
|
||||
});
|
||||
await user.click(configureButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/edit google authentication/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/domain/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/client id/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/client secret/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/skip email verification/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows workspace groups section when expanded', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockGoogleAuthWithWorkspaceGroups}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
const workspaceHeader = screen.getByText(/google workspace groups/i);
|
||||
await user.click(workspaceHeader);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/fetch groups/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/service account json/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SAML Provider', () => {
|
||||
it('shows SAML-specific fields when editing SAML domain', () => {
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockSamlAuthDomain}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/edit saml authentication/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByDisplayValue('https://idp.example.com/sso'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('urn:example:idp')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows attribute mapping section for SAML', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockSamlWithAttributeMapping}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(/attribute mapping \(advanced\)/i),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const attributeHeader = screen.getByText(/attribute mapping \(advanced\)/i);
|
||||
await user.click(attributeHeader);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/name attribute/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/groups attribute/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/role attribute/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OIDC Provider', () => {
|
||||
it('shows OIDC-specific fields when editing OIDC domain', () => {
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockOidcAuthDomain}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/edit oidc authentication/i)).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('https://oidc.corp.io')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('oidc-client-id')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows claim mapping section for OIDC', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockOidcWithClaimMapping}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/claim mapping \(advanced\)/i)).toBeInTheDocument();
|
||||
|
||||
const claimHeader = screen.getByText(/claim mapping \(advanced\)/i);
|
||||
await user.click(claimHeader);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/email claim/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/name claim/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/groups claim/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/role claim/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows OIDC options checkboxes', () => {
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockOidcAuthDomain}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/skip email verification/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/get user info/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Role Mapping', () => {
|
||||
it('shows role mapping section in provider forms', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<CreateEdit isCreate onClose={mockOnClose} />);
|
||||
|
||||
const configureButtons = await screen.findAllByRole('button', {
|
||||
name: /configure/i,
|
||||
});
|
||||
await user.click(configureButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/role mapping \(advanced\)/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('expands role mapping section to show default role selector', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockDomainWithRoleMapping}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
const roleMappingHeader = screen.getByText(/role mapping \(advanced\)/i);
|
||||
await user.click(roleMappingHeader);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/default role/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/use role attribute directly/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows group mappings section when useRoleAttribute is false', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockDomainWithRoleMapping}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
const roleMappingHeader = screen.getByText(/role mapping \(advanced\)/i);
|
||||
await user.click(roleMappingHeader);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/group to role mappings/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /add group mapping/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modal Actions', () => {
|
||||
it('calls onClose when cancel button is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<CreateEdit
|
||||
isCreate={false}
|
||||
record={mockGoogleAuthDomain}
|
||||
onClose={mockOnClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import Toggle from '../Toggle';
|
||||
import {
|
||||
AUTH_DOMAINS_UPDATE_ENDPOINT,
|
||||
mockErrorResponse,
|
||||
mockGoogleAuthDomain,
|
||||
mockUpdateSuccessResponse,
|
||||
} from './mocks';
|
||||
|
||||
describe('Toggle', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
it('renders switch with correct initial state', () => {
|
||||
render(<Toggle isDefaultChecked={true} record={mockGoogleAuthDomain} />);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
expect(switchElement).toBeChecked();
|
||||
});
|
||||
|
||||
it('renders unchecked switch when SSO is disabled', () => {
|
||||
render(
|
||||
<Toggle
|
||||
isDefaultChecked={false}
|
||||
record={{ ...mockGoogleAuthDomain, ssoEnabled: false }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
expect(switchElement).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('calls update API when toggle is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockUpdateAPI = jest.fn();
|
||||
|
||||
server.use(
|
||||
rest.put(AUTH_DOMAINS_UPDATE_ENDPOINT, async (req, res, ctx) => {
|
||||
const body = await req.json();
|
||||
mockUpdateAPI(body);
|
||||
return res(ctx.status(200), ctx.json(mockUpdateSuccessResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
render(<Toggle isDefaultChecked={true} record={mockGoogleAuthDomain} />);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
await user.click(switchElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(switchElement).not.toBeChecked();
|
||||
});
|
||||
|
||||
expect(mockUpdateAPI).toHaveBeenCalledTimes(1);
|
||||
expect(mockUpdateAPI).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
ssoEnabled: false,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('shows error modal when update fails', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
server.use(
|
||||
rest.put(AUTH_DOMAINS_UPDATE_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(500), ctx.json(mockErrorResponse)),
|
||||
),
|
||||
);
|
||||
|
||||
render(<Toggle isDefaultChecked={true} record={mockGoogleAuthDomain} />);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
await user.click(switchElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/failed to perform operation/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not call API when record has no id', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
let apiCalled = false;
|
||||
|
||||
server.use(
|
||||
rest.put(AUTH_DOMAINS_UPDATE_ENDPOINT, (_, res, ctx) => {
|
||||
apiCalled = true;
|
||||
return res(ctx.status(200), ctx.json(mockUpdateSuccessResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<Toggle
|
||||
isDefaultChecked={true}
|
||||
record={{ ...mockGoogleAuthDomain, id: undefined }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const switchElement = screen.getByRole('switch');
|
||||
await user.click(switchElement);
|
||||
|
||||
// Wait a bit to ensure no API call was made
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
expect(apiCalled).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
import { AuthtypesGettableAuthDomainDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
// API Endpoints
|
||||
export const AUTH_DOMAINS_LIST_ENDPOINT = '*/api/v1/domains';
|
||||
export const AUTH_DOMAINS_CREATE_ENDPOINT = '*/api/v1/domains';
|
||||
export const AUTH_DOMAINS_UPDATE_ENDPOINT = '*/api/v1/domains/:id';
|
||||
export const AUTH_DOMAINS_DELETE_ENDPOINT = '*/api/v1/domains/:id';
|
||||
|
||||
// Mock Auth Domain with Google Auth
|
||||
export const mockGoogleAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-1',
|
||||
name: 'signoz.io',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'google_auth',
|
||||
googleAuthConfig: {
|
||||
clientId: 'test-client-id',
|
||||
clientSecret: 'test-client-secret',
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-1',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock Auth Domain with SAML
|
||||
export const mockSamlAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-2',
|
||||
name: 'example.com',
|
||||
ssoEnabled: false,
|
||||
ssoType: 'saml',
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.example.com/sso',
|
||||
samlEntity: 'urn:example:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE',
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-2',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock Auth Domain with OIDC
|
||||
export const mockOidcAuthDomain: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-3',
|
||||
name: 'corp.io',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'oidc',
|
||||
oidcConfig: {
|
||||
issuer: 'https://oidc.corp.io',
|
||||
clientId: 'oidc-client-id',
|
||||
clientSecret: 'oidc-client-secret',
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-3',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock Auth Domain with Role Mapping
|
||||
export const mockDomainWithRoleMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-4',
|
||||
name: 'enterprise.com',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'saml',
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.enterprise.com/sso',
|
||||
samlEntity: 'urn:enterprise:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE',
|
||||
},
|
||||
roleMapping: {
|
||||
defaultRole: 'EDITOR',
|
||||
useRoleAttribute: false,
|
||||
groupMappings: {
|
||||
'admin-group': 'ADMIN',
|
||||
'dev-team': 'EDITOR',
|
||||
viewers: 'VIEWER',
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-4',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock Auth Domain with useRoleAttribute enabled
|
||||
export const mockDomainWithDirectRoleAttribute: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-5',
|
||||
name: 'direct-role.com',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'oidc',
|
||||
oidcConfig: {
|
||||
issuer: 'https://oidc.direct-role.com',
|
||||
clientId: 'direct-role-client-id',
|
||||
clientSecret: 'direct-role-client-secret',
|
||||
},
|
||||
roleMapping: {
|
||||
defaultRole: 'VIEWER',
|
||||
useRoleAttribute: true,
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-5',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock OIDC domain with claim mapping
|
||||
export const mockOidcWithClaimMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-6',
|
||||
name: 'oidc-claims.com',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'oidc',
|
||||
oidcConfig: {
|
||||
issuer: 'https://oidc.claims.com',
|
||||
issuerAlias: 'https://alias.claims.com',
|
||||
clientId: 'claims-client-id',
|
||||
clientSecret: 'claims-client-secret',
|
||||
insecureSkipEmailVerified: true,
|
||||
getUserInfo: true,
|
||||
claimMapping: {
|
||||
email: 'user_email',
|
||||
name: 'display_name',
|
||||
groups: 'user_groups',
|
||||
role: 'user_role',
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-6',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock SAML domain with attribute mapping
|
||||
export const mockSamlWithAttributeMapping: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-7',
|
||||
name: 'saml-attrs.com',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'saml',
|
||||
samlConfig: {
|
||||
samlIdp: 'https://idp.saml-attrs.com/sso',
|
||||
samlEntity: 'urn:saml-attrs:idp',
|
||||
samlCert: 'MOCK_CERTIFICATE_ATTRS',
|
||||
insecureSkipAuthNRequestsSigned: true,
|
||||
attributeMapping: {
|
||||
name: 'user_display_name',
|
||||
groups: 'member_of',
|
||||
role: 'signoz_role',
|
||||
},
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-7',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock Google Auth with workspace groups
|
||||
export const mockGoogleAuthWithWorkspaceGroups: AuthtypesGettableAuthDomainDTO = {
|
||||
id: 'domain-8',
|
||||
name: 'google-groups.com',
|
||||
ssoEnabled: true,
|
||||
ssoType: 'google_auth',
|
||||
googleAuthConfig: {
|
||||
clientId: 'google-groups-client-id',
|
||||
clientSecret: 'google-groups-client-secret',
|
||||
insecureSkipEmailVerified: false,
|
||||
fetchGroups: true,
|
||||
serviceAccountJson: '{"type": "service_account"}',
|
||||
domainToAdminEmail: {
|
||||
'google-groups.com': 'admin@google-groups.com',
|
||||
},
|
||||
fetchTransitiveGroupMembership: true,
|
||||
allowedGroups: ['allowed-group-1', 'allowed-group-2'],
|
||||
},
|
||||
authNProviderInfo: {
|
||||
relayStatePath: 'api/v1/sso/relay/domain-8',
|
||||
},
|
||||
};
|
||||
|
||||
// Mock empty list response
|
||||
export const mockEmptyDomainsResponse = {
|
||||
status: 'success',
|
||||
data: [],
|
||||
};
|
||||
|
||||
// Mock list response with domains
|
||||
export const mockDomainsListResponse = {
|
||||
status: 'success',
|
||||
data: [mockGoogleAuthDomain, mockSamlAuthDomain, mockOidcAuthDomain],
|
||||
};
|
||||
|
||||
// Mock single domain list response
|
||||
export const mockSingleDomainResponse = {
|
||||
status: 'success',
|
||||
data: [mockGoogleAuthDomain],
|
||||
};
|
||||
|
||||
// Mock success responses
|
||||
export const mockCreateSuccessResponse = {
|
||||
status: 'success',
|
||||
data: mockGoogleAuthDomain,
|
||||
};
|
||||
|
||||
export const mockUpdateSuccessResponse = {
|
||||
status: 'success',
|
||||
data: { ...mockGoogleAuthDomain, ssoEnabled: false },
|
||||
};
|
||||
|
||||
export const mockDeleteSuccessResponse = {
|
||||
status: 'success',
|
||||
data: 'Domain deleted successfully',
|
||||
};
|
||||
|
||||
// Mock error responses
|
||||
export const mockErrorResponse = {
|
||||
error: {
|
||||
code: 'internal_error',
|
||||
message: 'Failed to perform operation',
|
||||
url: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const mockValidationErrorResponse = {
|
||||
error: {
|
||||
code: 'invalid_input',
|
||||
message: 'Domain name is required',
|
||||
url: '',
|
||||
},
|
||||
};
|
||||
@@ -1,151 +1,193 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Table, Typography } from 'antd';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { Table } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import deleteDomain from 'api/v1/domains/id/delete';
|
||||
import listAllDomain from 'api/v1/domains/list';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import {
|
||||
useDeleteAuthDomain,
|
||||
useListAuthDomains,
|
||||
} from 'api/generated/services/authdomains';
|
||||
import {
|
||||
AuthtypesGettableAuthDomainDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import CopyToClipboard from 'periscope/components/CopyToClipboard';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { ErrorV2Resp } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
import { GettableAuthDomain, SSOType } from 'types/api/v1/domains/list';
|
||||
|
||||
import CreateEdit from './CreateEdit/CreateEdit';
|
||||
import Toggle from './Toggle';
|
||||
|
||||
import './AuthDomain.styles.scss';
|
||||
|
||||
const columns: ColumnsType<GettableAuthDomain> = [
|
||||
{
|
||||
title: 'Domain',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
render: (val): JSX.Element => <Typography.Text>{val}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: 'Enforce SSO',
|
||||
dataIndex: 'ssoEnabled',
|
||||
key: 'ssoEnabled',
|
||||
width: 80,
|
||||
render: (value: boolean, record: GettableAuthDomain): JSX.Element => (
|
||||
<Toggle isDefaultChecked={value} record={record} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'IDP Initiated SSO URL',
|
||||
dataIndex: 'relayState',
|
||||
key: 'relayState',
|
||||
width: 80,
|
||||
render: (_, record: GettableAuthDomain): JSX.Element => {
|
||||
const relayPath = record.authNProviderInfo.relayStatePath;
|
||||
if (!relayPath) {
|
||||
return (
|
||||
<Typography.Text style={{ paddingLeft: '6px' }}>N/A</Typography.Text>
|
||||
);
|
||||
}
|
||||
|
||||
const href = `${window.location.origin}/${relayPath}`;
|
||||
return <CopyToClipboard textToCopy={href} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
render: (_, record: GettableAuthDomain): JSX.Element => (
|
||||
<section className="auth-domain-list-column-action">
|
||||
<Typography.Link data-column-action="configure">
|
||||
Configure {SSOType.get(record.ssoType)}
|
||||
</Typography.Link>
|
||||
<Typography.Link type="danger" data-column-action="delete">
|
||||
Delete
|
||||
</Typography.Link>
|
||||
</section>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
async function deleteDomainById(
|
||||
id: string,
|
||||
showErrorModal: (error: APIError) => void,
|
||||
refetchAuthDomainListResponse: () => void,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await deleteDomain(id);
|
||||
refetchAuthDomainListResponse();
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
}
|
||||
}
|
||||
export const SSOType = new Map<string, string>([
|
||||
['google_auth', 'Google Auth'],
|
||||
['saml', 'SAML'],
|
||||
['email_password', 'Email Password'],
|
||||
['oidc', 'OIDC'],
|
||||
]);
|
||||
|
||||
function AuthDomain(): JSX.Element {
|
||||
const [record, setRecord] = useState<GettableAuthDomain>();
|
||||
const [record, setRecord] = useState<AuthtypesGettableAuthDomainDTO>();
|
||||
const [addDomain, setAddDomain] = useState<boolean>(false);
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const {
|
||||
data: authDomainListResponse,
|
||||
isLoading: isLoadingAuthDomainListResponse,
|
||||
isFetching: isFetchingAuthDomainListResponse,
|
||||
error: errorFetchingAuthDomainListResponse,
|
||||
refetch: refetchAuthDomainListResponse,
|
||||
} = useQuery({
|
||||
queryFn: listAllDomain,
|
||||
queryKey: ['/api/v1/domains', 'list'],
|
||||
enabled: true,
|
||||
});
|
||||
} = useListAuthDomains();
|
||||
|
||||
const { mutate: deleteAuthDomain } = useDeleteAuthDomain<
|
||||
AxiosError<RenderErrorResponseDTO>
|
||||
>();
|
||||
|
||||
const handleDeleteDomain = useCallback(
|
||||
(id: string): void => {
|
||||
deleteAuthDomain(
|
||||
{ pathParams: { id } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: 'Domain deleted successfully',
|
||||
});
|
||||
refetchAuthDomainListResponse();
|
||||
},
|
||||
onError: (error) => {
|
||||
try {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
} catch (apiError) {
|
||||
showErrorModal(apiError as APIError);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
[
|
||||
deleteAuthDomain,
|
||||
notifications,
|
||||
refetchAuthDomainListResponse,
|
||||
showErrorModal,
|
||||
],
|
||||
);
|
||||
|
||||
const formattedError = useMemo(() => {
|
||||
if (!errorFetchingAuthDomainListResponse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let errorResult: APIError | null = null;
|
||||
try {
|
||||
ErrorResponseHandlerV2(
|
||||
errorFetchingAuthDomainListResponse as AxiosError<ErrorV2Resp>,
|
||||
);
|
||||
} catch (error) {
|
||||
errorResult = error as APIError;
|
||||
}
|
||||
return errorResult;
|
||||
}, [errorFetchingAuthDomainListResponse]);
|
||||
|
||||
const columns: ColumnsType<AuthtypesGettableAuthDomainDTO> = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'Domain',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
render: (val): JSX.Element => <span>{val}</span>,
|
||||
},
|
||||
{
|
||||
title: 'Enforce SSO',
|
||||
dataIndex: 'ssoEnabled',
|
||||
key: 'ssoEnabled',
|
||||
width: 80,
|
||||
render: (
|
||||
value: boolean,
|
||||
record: AuthtypesGettableAuthDomainDTO,
|
||||
): JSX.Element => <Toggle isDefaultChecked={value} record={record} />,
|
||||
},
|
||||
{
|
||||
title: 'IDP Initiated SSO URL',
|
||||
dataIndex: 'relayState',
|
||||
key: 'relayState',
|
||||
width: 80,
|
||||
render: (_, record: AuthtypesGettableAuthDomainDTO): JSX.Element => {
|
||||
const relayPath = record.authNProviderInfo?.relayStatePath;
|
||||
if (!relayPath) {
|
||||
return <span className="auth-domain-list-na">N/A</span>;
|
||||
}
|
||||
|
||||
const href = `${window.location.origin}/${relayPath}`;
|
||||
return <CopyToClipboard textToCopy={href} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 100,
|
||||
render: (_, record: AuthtypesGettableAuthDomainDTO): JSX.Element => (
|
||||
<section className="auth-domain-list-column-action">
|
||||
<Button
|
||||
className="auth-domain-list-action-link"
|
||||
onClick={(): void => setRecord(record)}
|
||||
variant="link"
|
||||
>
|
||||
Configure {SSOType.get(record.ssoType || '')}
|
||||
</Button>
|
||||
<Button
|
||||
className="auth-domain-list-action-link delete"
|
||||
onClick={(): void => {
|
||||
if (record.id) {
|
||||
handleDeleteDomain(record.id);
|
||||
}
|
||||
}}
|
||||
variant="link"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</section>
|
||||
),
|
||||
},
|
||||
],
|
||||
[handleDeleteDomain],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="auth-domain">
|
||||
<section className="auth-domain-header">
|
||||
<Typography.Title level={3}>Authenticated Domains</Typography.Title>
|
||||
<h3 className="auth-domain-title">Authenticated Domains</h3>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
prefixIcon={<PlusOutlined />}
|
||||
onClick={(): void => {
|
||||
setAddDomain(true);
|
||||
}}
|
||||
className="button"
|
||||
variant="solid"
|
||||
size="sm"
|
||||
color="primary"
|
||||
>
|
||||
Add Domain
|
||||
</Button>
|
||||
</section>
|
||||
{(errorFetchingAuthDomainListResponse as APIError) && (
|
||||
<ErrorContent error={errorFetchingAuthDomainListResponse as APIError} />
|
||||
)}
|
||||
{!(errorFetchingAuthDomainListResponse as APIError) && (
|
||||
{formattedError && <ErrorContent error={formattedError} />}
|
||||
{!errorFetchingAuthDomainListResponse && (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={authDomainListResponse?.data}
|
||||
onRow={(record): any => ({
|
||||
onClick: (
|
||||
event: React.SyntheticEvent<HTMLLinkElement, MouseEvent>,
|
||||
): void => {
|
||||
const target = event.target as HTMLLinkElement;
|
||||
const { columnAction } = target.dataset;
|
||||
switch (columnAction) {
|
||||
case 'configure':
|
||||
setRecord(record);
|
||||
|
||||
break;
|
||||
case 'delete':
|
||||
deleteDomainById(
|
||||
record.id,
|
||||
showErrorModal,
|
||||
refetchAuthDomainListResponse,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.error('Unknown action:', columnAction);
|
||||
}
|
||||
},
|
||||
})}
|
||||
dataSource={authDomainListResponse?.data?.data}
|
||||
onRow={undefined}
|
||||
loading={
|
||||
isLoadingAuthDomainListResponse || isFetchingAuthDomainListResponse
|
||||
}
|
||||
className="auth-domain-list"
|
||||
rowKey="id"
|
||||
/>
|
||||
)}
|
||||
{(addDomain || record) && (
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import BarPanel from 'container/DashboardContainer/visualization/panels/BarPanel/BarPanel';
|
||||
|
||||
import TimeSeriesPanel from '../DashboardContainer/visualization/panels/TimeSeriesPanel/TimeSeriesPanel';
|
||||
import HistogramPanelWrapper from './HistogramPanelWrapper';
|
||||
import ListPanelWrapper from './ListPanelWrapper';
|
||||
import PiePanelWrapper from './PiePanelWrapper';
|
||||
import TablePanelWrapper from './TablePanelWrapper';
|
||||
import UplotPanelWrapper from './UplotPanelWrapper';
|
||||
import ValuePanelWrapper from './ValuePanelWrapper';
|
||||
|
||||
export const PanelTypeVsPanelWrapper = {
|
||||
@@ -16,7 +16,7 @@ export const PanelTypeVsPanelWrapper = {
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||
[PANEL_TYPES.PIE]: PiePanelWrapper,
|
||||
[PANEL_TYPES.BAR]: UplotPanelWrapper,
|
||||
[PANEL_TYPES.BAR]: BarPanel,
|
||||
[PANEL_TYPES.HISTOGRAM]: HistogramPanelWrapper,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SeriesVisibilityState } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { SeriesVisibilityItem } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { getStoredSeriesVisibility } from 'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils';
|
||||
import { ThresholdsDrawHookOptions } from 'lib/uPlotV2/hooks/types';
|
||||
import { thresholdsDrawHook } from 'lib/uPlotV2/hooks/useThresholdsDrawHook';
|
||||
@@ -238,7 +238,7 @@ export class UPlotConfigBuilder extends ConfigBuilder<
|
||||
/**
|
||||
* Returns stored series visibility by index from localStorage when preferences source is LOCAL_STORAGE, otherwise null.
|
||||
*/
|
||||
private getStoredVisibility(): SeriesVisibilityState | null {
|
||||
private getStoredVisibility(): SeriesVisibilityItem[] | null {
|
||||
if (
|
||||
this.widgetId &&
|
||||
this.selectionPreferencesSource === SelectionPreferencesSource.LOCAL_STORAGE
|
||||
@@ -248,14 +248,98 @@ export class UPlotConfigBuilder extends ConfigBuilder<
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive visibility resolution state from stored preferences and current series:
|
||||
* - visibleStoredLabels: labels that should always be visible
|
||||
* - hiddenStoredLabels: labels that should always be hidden
|
||||
* - hasActivePreference: whether a "mix" preference applies to new labels
|
||||
*/
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
private getVisibilityResolutionState(): {
|
||||
visibleStoredLabels: Set<string>;
|
||||
hiddenStoredLabels: Set<string>;
|
||||
hasActivePreference: boolean;
|
||||
} {
|
||||
const seriesVisibilityState = this.getStoredVisibility();
|
||||
if (!seriesVisibilityState || seriesVisibilityState.length === 0) {
|
||||
return {
|
||||
visibleStoredLabels: new Set<string>(),
|
||||
hiddenStoredLabels: new Set<string>(),
|
||||
hasActivePreference: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Single pass over stored items to derive:
|
||||
// - visibleStoredLabels: any label that is ever stored as visible
|
||||
// - hiddenStoredLabels: labels that are only ever stored as hidden
|
||||
// - hasMixPreference: there is at least one visible and one hidden entry
|
||||
const visibleStoredLabels = new Set<string>();
|
||||
const hiddenStoredLabels = new Set<string>();
|
||||
let hasAnyVisible = false;
|
||||
let hasAnyHidden = false;
|
||||
|
||||
for (const { label, show } of seriesVisibilityState) {
|
||||
if (show) {
|
||||
hasAnyVisible = true;
|
||||
visibleStoredLabels.add(label);
|
||||
// If a label is ever visible, it should not be treated as "only hidden"
|
||||
if (hiddenStoredLabels.has(label)) {
|
||||
hiddenStoredLabels.delete(label);
|
||||
}
|
||||
} else {
|
||||
hasAnyHidden = true;
|
||||
// Only track as hidden if we have not already seen it as visible
|
||||
if (!visibleStoredLabels.has(label)) {
|
||||
hiddenStoredLabels.add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasMixPreference = hasAnyVisible && hasAnyHidden;
|
||||
|
||||
// Current series labels in this chart.
|
||||
const currentSeriesLabels = this.series.map(
|
||||
(s: UPlotSeriesBuilder) => s.getConfig().label ?? '',
|
||||
);
|
||||
|
||||
// Check if any stored "visible" label exists in the current series list.
|
||||
const hasVisibleIntersection =
|
||||
visibleStoredLabels.size > 0 &&
|
||||
currentSeriesLabels.some((label) => visibleStoredLabels.has(label));
|
||||
|
||||
// Active preference only when there is a mix AND at least one visible
|
||||
// stored label is present in the current series list.
|
||||
const hasActivePreference = hasMixPreference && hasVisibleIntersection;
|
||||
|
||||
// We apply stored visibility in two cases:
|
||||
// - There is an active preference (mix + intersection), OR
|
||||
// - There is no mix (all true or all false) – preserve legacy behavior.
|
||||
const shouldApplyStoredVisibility = !hasMixPreference || hasActivePreference;
|
||||
|
||||
if (!shouldApplyStoredVisibility) {
|
||||
return {
|
||||
visibleStoredLabels: new Set<string>(),
|
||||
hiddenStoredLabels: new Set<string>(),
|
||||
hasActivePreference,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
visibleStoredLabels,
|
||||
hiddenStoredLabels,
|
||||
hasActivePreference,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get legend items with visibility state restored from localStorage if available
|
||||
*/
|
||||
getLegendItems(): Record<number, LegendItem> {
|
||||
const seriesVisibilityState = this.getStoredVisibility();
|
||||
const isAnySeriesHidden = !!seriesVisibilityState?.visibility?.some(
|
||||
(show) => !show,
|
||||
);
|
||||
const {
|
||||
visibleStoredLabels,
|
||||
hiddenStoredLabels,
|
||||
hasActivePreference,
|
||||
} = this.getVisibilityResolutionState();
|
||||
|
||||
return this.series.reduce((acc, s: UPlotSeriesBuilder, index: number) => {
|
||||
const seriesConfig = s.getConfig();
|
||||
@@ -263,11 +347,11 @@ export class UPlotConfigBuilder extends ConfigBuilder<
|
||||
// +1 because uPlot series 0 is x-axis/time; data series are at 1, 2, ... (also matches stored visibility[0]=time, visibility[1]=first data, ...)
|
||||
const seriesIndex = index + 1;
|
||||
const show = resolveSeriesVisibility({
|
||||
seriesIndex,
|
||||
seriesShow: seriesConfig.show,
|
||||
seriesLabel: label,
|
||||
seriesVisibilityState,
|
||||
isAnySeriesHidden,
|
||||
visibleStoredLabels,
|
||||
hiddenStoredLabels,
|
||||
hasActivePreference,
|
||||
});
|
||||
|
||||
acc[seriesIndex] = {
|
||||
@@ -296,22 +380,23 @@ export class UPlotConfigBuilder extends ConfigBuilder<
|
||||
...DEFAULT_PLOT_CONFIG,
|
||||
};
|
||||
|
||||
const seriesVisibilityState = this.getStoredVisibility();
|
||||
const isAnySeriesHidden = !!seriesVisibilityState?.visibility?.some(
|
||||
(show) => !show,
|
||||
);
|
||||
const {
|
||||
visibleStoredLabels,
|
||||
hiddenStoredLabels,
|
||||
hasActivePreference,
|
||||
} = this.getVisibilityResolutionState();
|
||||
|
||||
config.series = [
|
||||
{ value: (): string => '' }, // Base series for timestamp
|
||||
...this.series.map((s, index) => {
|
||||
...this.series.map((s) => {
|
||||
const series = s.getConfig();
|
||||
// Stored visibility[0] is x-axis/time; data series start at visibility[1]
|
||||
const visible = resolveSeriesVisibility({
|
||||
seriesIndex: index + 1,
|
||||
seriesShow: series.show,
|
||||
seriesLabel: series.label ?? '',
|
||||
seriesVisibilityState,
|
||||
isAnySeriesHidden,
|
||||
visibleStoredLabels,
|
||||
hiddenStoredLabels,
|
||||
hasActivePreference,
|
||||
});
|
||||
return {
|
||||
...series,
|
||||
|
||||
@@ -186,11 +186,10 @@ describe('UPlotConfigBuilder', () => {
|
||||
});
|
||||
|
||||
it('restores visibility state from localStorage when selectionPreferencesSource is LOCAL_STORAGE', () => {
|
||||
// Index 0 = x-axis/time; indices 1,2 = data series (Requests, Errors). resolveSeriesVisibility matches by seriesIndex + seriesLabel.
|
||||
getStoredSeriesVisibilityMock.getStoredSeriesVisibility.mockReturnValue({
|
||||
labels: ['x-axis', 'Requests', 'Errors'],
|
||||
visibility: [true, true, false],
|
||||
});
|
||||
getStoredSeriesVisibilityMock.getStoredSeriesVisibility.mockReturnValue([
|
||||
{ label: 'Requests', show: true },
|
||||
{ label: 'Errors', show: false },
|
||||
]);
|
||||
|
||||
const builder = new UPlotConfigBuilder({
|
||||
widgetId: 'widget-1',
|
||||
@@ -202,7 +201,7 @@ describe('UPlotConfigBuilder', () => {
|
||||
|
||||
const legendItems = builder.getLegendItems();
|
||||
|
||||
// When any series is hidden, legend visibility is driven by the stored map
|
||||
// When any series is hidden, visibility is driven by stored label-based preferences
|
||||
expect(legendItems[1].show).toBe(true);
|
||||
expect(legendItems[2].show).toBe(false);
|
||||
|
||||
@@ -213,6 +212,109 @@ describe('UPlotConfigBuilder', () => {
|
||||
expect(secondSeries?.show).toBe(false);
|
||||
});
|
||||
|
||||
it('hides new series by default when there is a mixed preference and a visible label matches current series', () => {
|
||||
getStoredSeriesVisibilityMock.getStoredSeriesVisibility.mockReturnValue([
|
||||
{ label: 'Requests', show: true },
|
||||
{ label: 'Errors', show: false },
|
||||
]);
|
||||
|
||||
const builder = new UPlotConfigBuilder({
|
||||
widgetId: 'widget-1',
|
||||
selectionPreferencesSource: SelectionPreferencesSource.LOCAL_STORAGE,
|
||||
});
|
||||
|
||||
builder.addSeries(createSeriesProps({ label: 'Requests' }));
|
||||
builder.addSeries(createSeriesProps({ label: 'Errors' }));
|
||||
builder.addSeries(createSeriesProps({ label: 'Latency' }));
|
||||
|
||||
const legendItems = builder.getLegendItems();
|
||||
|
||||
// Stored labels: Requests (visible), Errors (hidden).
|
||||
// New label "Latency" should be hidden because there is a mixed preference
|
||||
// and "Requests" (a visible stored label) is present in the current series.
|
||||
expect(legendItems[1].label).toBe('Requests');
|
||||
expect(legendItems[1].show).toBe(true);
|
||||
expect(legendItems[2].label).toBe('Errors');
|
||||
expect(legendItems[2].show).toBe(false);
|
||||
expect(legendItems[3].label).toBe('Latency');
|
||||
expect(legendItems[3].show).toBe(false);
|
||||
|
||||
const config = builder.getConfig();
|
||||
const [, firstSeries, secondSeries, thirdSeries] = config.series ?? [];
|
||||
|
||||
expect(firstSeries?.label).toBe('Requests');
|
||||
expect(firstSeries?.show).toBe(true);
|
||||
expect(secondSeries?.label).toBe('Errors');
|
||||
expect(secondSeries?.show).toBe(false);
|
||||
expect(thirdSeries?.label).toBe('Latency');
|
||||
expect(thirdSeries?.show).toBe(false);
|
||||
});
|
||||
|
||||
it('shows all series when there is a mixed preference but no visible stored labels match current series', () => {
|
||||
getStoredSeriesVisibilityMock.getStoredSeriesVisibility.mockReturnValue([
|
||||
{ label: 'StoredVisible', show: true },
|
||||
{ label: 'StoredHidden', show: false },
|
||||
]);
|
||||
|
||||
const builder = new UPlotConfigBuilder({
|
||||
widgetId: 'widget-1',
|
||||
selectionPreferencesSource: SelectionPreferencesSource.LOCAL_STORAGE,
|
||||
});
|
||||
|
||||
// None of these labels intersect with the stored visible label "StoredVisible"
|
||||
builder.addSeries(createSeriesProps({ label: 'CPU' }));
|
||||
builder.addSeries(createSeriesProps({ label: 'Memory' }));
|
||||
|
||||
const legendItems = builder.getLegendItems();
|
||||
|
||||
// Mixed preference exists in storage, but since no visible labels intersect
|
||||
// with current series, stored preferences are ignored and all are visible.
|
||||
expect(legendItems[1].label).toBe('CPU');
|
||||
expect(legendItems[1].show).toBe(true);
|
||||
expect(legendItems[2].label).toBe('Memory');
|
||||
expect(legendItems[2].show).toBe(true);
|
||||
|
||||
const config = builder.getConfig();
|
||||
const [, firstSeries, secondSeries] = config.series ?? [];
|
||||
|
||||
expect(firstSeries?.label).toBe('CPU');
|
||||
expect(firstSeries?.show).toBe(true);
|
||||
expect(secondSeries?.label).toBe('Memory');
|
||||
expect(secondSeries?.show).toBe(true);
|
||||
});
|
||||
|
||||
it('treats duplicate labels as visible when any stored entry for that label is visible', () => {
|
||||
getStoredSeriesVisibilityMock.getStoredSeriesVisibility.mockReturnValue([
|
||||
{ label: 'CPU', show: true },
|
||||
{ label: 'CPU', show: false },
|
||||
]);
|
||||
|
||||
const builder = new UPlotConfigBuilder({
|
||||
widgetId: 'widget-dup',
|
||||
selectionPreferencesSource: SelectionPreferencesSource.LOCAL_STORAGE,
|
||||
});
|
||||
|
||||
// Two series with the same label; both should be visible because at least
|
||||
// one stored entry for "CPU" is visible.
|
||||
builder.addSeries(createSeriesProps({ label: 'CPU' }));
|
||||
builder.addSeries(createSeriesProps({ label: 'CPU' }));
|
||||
|
||||
const legendItems = builder.getLegendItems();
|
||||
|
||||
expect(legendItems[1].label).toBe('CPU');
|
||||
expect(legendItems[1].show).toBe(true);
|
||||
expect(legendItems[2].label).toBe('CPU');
|
||||
expect(legendItems[2].show).toBe(true);
|
||||
|
||||
const config = builder.getConfig();
|
||||
const [, firstSeries, secondSeries] = config.series ?? [];
|
||||
|
||||
expect(firstSeries?.label).toBe('CPU');
|
||||
expect(firstSeries?.show).toBe(true);
|
||||
expect(secondSeries?.label).toBe('CPU');
|
||||
expect(secondSeries?.show).toBe(true);
|
||||
});
|
||||
|
||||
it('does not attempt to read stored visibility when using in-memory preferences', () => {
|
||||
const builder = new UPlotConfigBuilder({
|
||||
widgetId: 'widget-1',
|
||||
|
||||
@@ -1,25 +1,44 @@
|
||||
import { SeriesVisibilityState } from 'container/DashboardContainer/visualization/panels/types';
|
||||
|
||||
/**
|
||||
* Resolve the visibility of a single series based on:
|
||||
* - Stored per-series visibility (when applicable)
|
||||
* - Whether there is an "active preference" (mix of visible/hidden that matches current series)
|
||||
* - The series' own default show flag
|
||||
*/
|
||||
export function resolveSeriesVisibility({
|
||||
seriesIndex,
|
||||
seriesShow,
|
||||
seriesLabel,
|
||||
seriesVisibilityState,
|
||||
isAnySeriesHidden,
|
||||
visibleStoredLabels,
|
||||
hiddenStoredLabels,
|
||||
hasActivePreference,
|
||||
}: {
|
||||
seriesIndex: number;
|
||||
seriesShow: boolean | undefined | null;
|
||||
seriesLabel: string;
|
||||
seriesVisibilityState: SeriesVisibilityState | null;
|
||||
isAnySeriesHidden: boolean;
|
||||
visibleStoredLabels: Set<string> | null;
|
||||
hiddenStoredLabels: Set<string> | null;
|
||||
hasActivePreference: boolean;
|
||||
}): boolean {
|
||||
if (
|
||||
isAnySeriesHidden &&
|
||||
seriesVisibilityState?.visibility &&
|
||||
seriesVisibilityState.labels.length > seriesIndex &&
|
||||
seriesVisibilityState.labels[seriesIndex] === seriesLabel
|
||||
) {
|
||||
return seriesVisibilityState.visibility[seriesIndex] ?? false;
|
||||
const isStoredVisible = !!visibleStoredLabels?.has(seriesLabel);
|
||||
const isStoredHidden = !!hiddenStoredLabels?.has(seriesLabel);
|
||||
|
||||
// If the label is explicitly stored as visible, always show it.
|
||||
if (isStoredVisible) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the label is explicitly stored as hidden (and never stored as visible),
|
||||
// always hide it.
|
||||
if (isStoredHidden) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// "Active preference" means:
|
||||
// - There is a mix of visible/hidden in storage, AND
|
||||
// - At least one stored *visible* label exists in the current series list.
|
||||
// For such a preference, any new/unknown series should be hidden by default.
|
||||
if (hasActivePreference) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise fall back to the series' own config or show by default.
|
||||
return seriesShow ?? true;
|
||||
}
|
||||
|
||||
@@ -204,6 +204,78 @@ describe('dashboardVariablesStore', () => {
|
||||
expect(doAllVariablesHaveValuesSelected).toBe(true);
|
||||
});
|
||||
|
||||
it('should treat DYNAMIC variable with allSelected=true and selectedValue=undefined as having a value', () => {
|
||||
setDashboardVariablesStore({
|
||||
dashboardId: 'dash-1',
|
||||
variables: {
|
||||
dyn1: createVariable({
|
||||
name: 'dyn1',
|
||||
type: 'DYNAMIC',
|
||||
order: 0,
|
||||
selectedValue: undefined,
|
||||
allSelected: true,
|
||||
}),
|
||||
env: createVariable({
|
||||
name: 'env',
|
||||
type: 'QUERY',
|
||||
order: 1,
|
||||
selectedValue: 'prod',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const { doAllVariablesHaveValuesSelected } = getVariableDependencyContext();
|
||||
expect(doAllVariablesHaveValuesSelected).toBe(true);
|
||||
});
|
||||
|
||||
it('should treat DYNAMIC variable with allSelected=true and empty string selectedValue as having a value', () => {
|
||||
setDashboardVariablesStore({
|
||||
dashboardId: 'dash-1',
|
||||
variables: {
|
||||
dyn1: createVariable({
|
||||
name: 'dyn1',
|
||||
type: 'DYNAMIC',
|
||||
order: 0,
|
||||
selectedValue: '',
|
||||
allSelected: true,
|
||||
}),
|
||||
env: createVariable({
|
||||
name: 'env',
|
||||
type: 'QUERY',
|
||||
order: 1,
|
||||
selectedValue: 'prod',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const { doAllVariablesHaveValuesSelected } = getVariableDependencyContext();
|
||||
expect(doAllVariablesHaveValuesSelected).toBe(true);
|
||||
});
|
||||
|
||||
it('should treat DYNAMIC variable with allSelected=true and empty array selectedValue as having a value', () => {
|
||||
setDashboardVariablesStore({
|
||||
dashboardId: 'dash-1',
|
||||
variables: {
|
||||
dyn1: createVariable({
|
||||
name: 'dyn1',
|
||||
type: 'DYNAMIC',
|
||||
order: 0,
|
||||
selectedValue: [] as any,
|
||||
allSelected: true,
|
||||
}),
|
||||
env: createVariable({
|
||||
name: 'env',
|
||||
type: 'QUERY',
|
||||
order: 1,
|
||||
selectedValue: 'prod',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const { doAllVariablesHaveValuesSelected } = getVariableDependencyContext();
|
||||
expect(doAllVariablesHaveValuesSelected).toBe(true);
|
||||
});
|
||||
|
||||
it('should report false when a DYNAMIC variable has empty selectedValue and allSelected is not true', () => {
|
||||
setDashboardVariablesStore({
|
||||
dashboardId: 'dash-1',
|
||||
|
||||
@@ -76,7 +76,7 @@ export function getVariableDependencyContext(): VariableFetchContext {
|
||||
(variable) => {
|
||||
if (
|
||||
variable.type === 'DYNAMIC' &&
|
||||
variable.selectedValue === null &&
|
||||
(variable.selectedValue === null || isEmpty(variable.selectedValue)) &&
|
||||
variable.allSelected === true
|
||||
) {
|
||||
return true;
|
||||
|
||||
@@ -7,4 +7,5 @@ export interface UpdateProfileProps {
|
||||
number_of_services: number;
|
||||
number_of_hosts: number;
|
||||
where_did_you_discover_signoz: string;
|
||||
timeline_for_migrating_to_signoz: string;
|
||||
}
|
||||
|
||||
@@ -4073,6 +4073,16 @@
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-slot" "1.0.2"
|
||||
|
||||
"@radix-ui/react-collection@1.1.7":
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz#d05c25ca9ac4695cc19ba91f42f686e3ea2d9aec"
|
||||
integrity sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs" "1.1.2"
|
||||
"@radix-ui/react-context" "1.1.2"
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
"@radix-ui/react-slot" "1.2.3"
|
||||
|
||||
"@radix-ui/react-compose-refs@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae"
|
||||
@@ -4159,6 +4169,11 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-direction@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz#39e5a5769e676c753204b792fbe6cf508e550a14"
|
||||
integrity sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==
|
||||
|
||||
"@radix-ui/react-dismissable-layer@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz#35b7826fa262fd84370faef310e627161dffa76b"
|
||||
@@ -4387,6 +4402,22 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-slot" "1.2.4"
|
||||
|
||||
"@radix-ui/react-radio-group@^1.3.4":
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz#93f102b5b948d602c2f2adb1bc5c347cbaf64bd9"
|
||||
integrity sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==
|
||||
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-direction" "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-use-previous" "1.1.1"
|
||||
"@radix-ui/react-use-size" "1.1.1"
|
||||
|
||||
"@radix-ui/react-roving-focus@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974"
|
||||
@@ -4403,6 +4434,21 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
|
||||
"@radix-ui/react-roving-focus@1.1.11":
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz#ef54384b7361afc6480dcf9907ef2fedb5080fd9"
|
||||
integrity sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==
|
||||
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-id" "1.1.1"
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.1"
|
||||
"@radix-ui/react-use-controllable-state" "1.2.2"
|
||||
|
||||
"@radix-ui/react-slot@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.0.tgz#7fa805b99891dea1e862d8f8fbe07f4d6d0fd698"
|
||||
@@ -4433,6 +4479,19 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-compose-refs" "1.1.2"
|
||||
|
||||
"@radix-ui/react-switch@^1.1.4":
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.2.6.tgz#ff79acb831f0d5ea9216cfcc5b939912571358e3"
|
||||
integrity sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==
|
||||
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-primitive" "2.1.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.2.2"
|
||||
"@radix-ui/react-use-previous" "1.1.1"
|
||||
"@radix-ui/react-use-size" "1.1.1"
|
||||
|
||||
"@radix-ui/react-tabs@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2"
|
||||
@@ -5075,6 +5134,20 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/radio-group@0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/radio-group/-/radio-group-0.0.2.tgz#4b13567bfee2645226f2cf41f261bbb288e1be4b"
|
||||
integrity sha512-ahykmA5hPujOC964CFveMlQ12tWSyut2CUiFRqT1QxRkOLS2R44Qn2hh2psqJJ18JMX/24ZYCAIh9Bdd5XW+7g==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-radio-group" "^1.3.4"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lucide-react "^0.445.0"
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/resizable@0.0.0":
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/resizable/-/resizable-0.0.0.tgz#a517818b9f9bcdaeafc55ae134be86522bc90e9f"
|
||||
@@ -5104,6 +5177,20 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/switch@0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/switch/-/switch-0.0.2.tgz#58003ce9c0cd1f2ad8266a7045182607ce51df76"
|
||||
integrity sha512-3B3Y5dzIyepO6EQJ7agx97bPmwg1dcOY46q2lqviHnMxNk3Sv079nSNCaztjQlo0VR0qu2JgVXhWi5Lw9WBN8A==
|
||||
dependencies:
|
||||
"@radix-ui/react-icons" "^1.3.0"
|
||||
"@radix-ui/react-slot" "^1.1.0"
|
||||
"@radix-ui/react-switch" "^1.1.4"
|
||||
class-variance-authority "^0.7.0"
|
||||
clsx "^2.1.1"
|
||||
lucide-react "^0.445.0"
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/table@0.3.7":
|
||||
version "0.3.7"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/table/-/table-0.3.7.tgz#895b710c02af124dfb5117e02bbc6d80ce062063"
|
||||
|
||||
@@ -30,7 +30,3 @@ func (module *getter) ListByOwnedKeyRange(ctx context.Context) ([]*types.Organiz
|
||||
|
||||
return module.store.ListByKeyRange(ctx, start, end)
|
||||
}
|
||||
|
||||
func (module *getter) GetByName(ctx context.Context, name string) (*types.Organization, error) {
|
||||
return module.store.GetByName(ctx, name)
|
||||
}
|
||||
|
||||
@@ -47,22 +47,6 @@ func (store *store) Get(ctx context.Context, id valuer.UUID) (*types.Organizatio
|
||||
return organization, nil
|
||||
}
|
||||
|
||||
func (store *store) GetByName(ctx context.Context, name string) (*types.Organization, error) {
|
||||
organization := new(types.Organization)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(organization).
|
||||
Where("name = ?", name).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrOrganizationNotFound, "organization with name %s does not exist", name)
|
||||
}
|
||||
|
||||
return organization, nil
|
||||
}
|
||||
|
||||
func (store *store) GetAll(ctx context.Context) ([]*types.Organization, error) {
|
||||
organizations := make([]*types.Organization, 0)
|
||||
err := store.
|
||||
|
||||
@@ -14,9 +14,6 @@ type Getter interface {
|
||||
|
||||
// ListByOwnedKeyRange gets all the organizations owned by the instance
|
||||
ListByOwnedKeyRange(context.Context) ([]*types.Organization, error)
|
||||
|
||||
// Gets the organization by name
|
||||
GetByName(context.Context, string) (*types.Organization, error)
|
||||
}
|
||||
|
||||
type Setter interface {
|
||||
|
||||
@@ -151,10 +151,6 @@ func (module *module) CreateCallbackAuthNSession(ctx context.Context, authNProvi
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := user.ErrIfRoot(); err != nil {
|
||||
return "", errors.WithAdditionalf(err, "root user can only authenticate via password")
|
||||
}
|
||||
|
||||
token, err := module.tokenizer.CreateToken(ctx, authtypes.NewIdentity(user.ID, user.OrgID, user.Email, user.Role), map[string]string{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -5,22 +5,11 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Password PasswordConfig `mapstructure:"password"`
|
||||
Root RootConfig `mapstructure:"root"`
|
||||
}
|
||||
|
||||
type RootConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Email valuer.Email `mapstructure:"email"`
|
||||
Password string `mapstructure:"password"`
|
||||
OrgName string `mapstructure:"org_name"`
|
||||
}
|
||||
|
||||
type PasswordConfig struct {
|
||||
Reset ResetConfig `mapstructure:"reset"`
|
||||
}
|
||||
@@ -42,10 +31,6 @@ func newConfig() factory.Config {
|
||||
MaxTokenLifetime: 6 * time.Hour,
|
||||
},
|
||||
},
|
||||
Root: RootConfig{
|
||||
Enabled: false,
|
||||
OrgName: "default",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,17 +39,5 @@ func (c Config) Validate() error {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::password::reset::max_token_lifetime must be positive")
|
||||
}
|
||||
|
||||
if c.Root.Enabled {
|
||||
if c.Root.Email.IsZero() {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::root::email is required when root user is enabled")
|
||||
}
|
||||
if c.Root.Password == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::root::password is required when root user is enabled")
|
||||
}
|
||||
if !types.IsPasswordValid(c.Root.Password) {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "user::root::password does not meet password requirements")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,10 +16,6 @@ func NewGetter(store types.UserStore) user.Getter {
|
||||
return &getter{store: store}
|
||||
}
|
||||
|
||||
func (module *getter) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.User, error) {
|
||||
return module.store.GetRootUserByOrgID(ctx, orgID)
|
||||
}
|
||||
|
||||
func (module *getter) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.User, error) {
|
||||
users, err := module.store.ListUsersByOrgID(ctx, orgID)
|
||||
if err != nil {
|
||||
|
||||
@@ -103,12 +103,6 @@ func (m *Module) CreateBulkInvite(ctx context.Context, orgID valuer.UUID, userID
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existingUser != nil {
|
||||
if err := existingUser.ErrIfRoot(); err != nil {
|
||||
return nil, errors.WithAdditionalf(err, "cannot send invite to root user")
|
||||
}
|
||||
}
|
||||
|
||||
if existingUser != nil {
|
||||
return nil, errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "User already exists with the same email")
|
||||
}
|
||||
@@ -208,21 +202,27 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := existingUser.ErrIfRoot(); err != nil {
|
||||
return nil, errors.WithAdditionalf(err, "cannot update root user")
|
||||
}
|
||||
|
||||
requestor, err := m.store.GetUser(ctx, valuer.MustNewUUID(updatedBy))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.Role != "" && user.Role != existingUser.Role && requestor.Role != types.RoleAdmin {
|
||||
// only displayName, role can be updated
|
||||
if user.DisplayName == "" {
|
||||
user.DisplayName = existingUser.DisplayName
|
||||
}
|
||||
|
||||
if user.Role == "" {
|
||||
user.Role = existingUser.Role
|
||||
}
|
||||
|
||||
if 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 user.Role != "" && user.Role != existingUser.Role && existingUser.Role == types.RoleAdmin {
|
||||
// Make sure that th e request is not demoting the last admin user.
|
||||
// also an admin user can only change role of their own or other user
|
||||
if user.Role != existingUser.Role && existingUser.Role == types.RoleAdmin {
|
||||
adminUsers, err := m.store.GetUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -233,7 +233,7 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
|
||||
}
|
||||
}
|
||||
|
||||
if user.Role != "" && user.Role != existingUser.Role {
|
||||
if user.Role != existingUser.Role {
|
||||
err = m.authz.ModifyGrant(ctx,
|
||||
orgID,
|
||||
roletypes.MustGetSigNozManagedRoleFromExistingRole(existingUser.Role),
|
||||
@@ -245,28 +245,23 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u
|
||||
}
|
||||
}
|
||||
|
||||
existingUser.Update(user.DisplayName, user.Role)
|
||||
if err := m.UpdateAnyUser(ctx, orgID, existingUser); err != nil {
|
||||
user.UpdatedAt = time.Now()
|
||||
updatedUser, err := m.store.UpdateUser(ctx, orgID, id, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return existingUser, nil
|
||||
}
|
||||
traits := types.NewTraitsFromUser(updatedUser)
|
||||
m.analytics.IdentifyUser(ctx, user.OrgID.String(), user.ID.String(), traits)
|
||||
|
||||
func (module *Module) UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.User) error {
|
||||
if err := module.store.UpdateUser(ctx, orgID, user); err != nil {
|
||||
return err
|
||||
traits["updated_by"] = updatedBy
|
||||
m.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Updated", traits)
|
||||
|
||||
if err := m.tokenizer.DeleteIdentity(ctx, valuer.MustNewUUID(id)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
traits := types.NewTraitsFromUser(user)
|
||||
module.analytics.IdentifyUser(ctx, user.OrgID.String(), user.ID.String(), traits)
|
||||
module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Updated", traits)
|
||||
|
||||
if err := module.tokenizer.DeleteIdentity(ctx, user.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return updatedUser, nil
|
||||
}
|
||||
|
||||
func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error {
|
||||
@@ -275,10 +270,6 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
|
||||
return err
|
||||
}
|
||||
|
||||
if err := user.ErrIfRoot(); err != nil {
|
||||
return errors.WithAdditionalf(err, "cannot delete root user")
|
||||
}
|
||||
|
||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(user.Email.String())) {
|
||||
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "integration user cannot be deleted")
|
||||
}
|
||||
@@ -373,10 +364,6 @@ func (module *Module) ForgotPassword(ctx context.Context, orgID valuer.UUID, ema
|
||||
return err
|
||||
}
|
||||
|
||||
if err := user.ErrIfRoot(); err != nil {
|
||||
return errors.WithAdditionalf(err, "cannot reset password for root user")
|
||||
}
|
||||
|
||||
token, err := module.GetOrCreateResetPasswordToken(ctx, user.ID)
|
||||
if err != nil {
|
||||
module.settings.Logger().ErrorContext(ctx, "failed to create reset password token", "error", err)
|
||||
@@ -420,15 +407,6 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := module.store.GetUser(ctx, valuer.MustNewUUID(password.UserID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := user.ErrIfRoot(); err != nil {
|
||||
return errors.WithAdditionalf(err, "cannot reset password for root user")
|
||||
}
|
||||
|
||||
if err := password.Update(passwd); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -437,15 +415,6 @@ func (module *Module) UpdatePasswordByResetPasswordToken(ctx context.Context, to
|
||||
}
|
||||
|
||||
func (module *Module) UpdatePassword(ctx context.Context, userID valuer.UUID, oldpasswd string, passwd string) error {
|
||||
user, err := module.store.GetUser(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := user.ErrIfRoot(); err != nil {
|
||||
return errors.WithAdditionalf(err, "cannot change password for root user")
|
||||
}
|
||||
|
||||
password, err := module.store.GetPasswordByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -507,7 +476,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)
|
||||
user, err := types.NewUser(name, email, types.RoleAdmin, organization.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
package impluser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"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"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/roletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type service 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,
|
||||
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,
|
||||
module: module,
|
||||
orgGetter: orgGetter,
|
||||
authz: authz,
|
||||
config: config,
|
||||
stopC: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) Start(ctx context.Context) error {
|
||||
if !s.config.Enabled {
|
||||
<-s.stopC
|
||||
return nil
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
err := s.reconcile(ctx)
|
||||
if err == nil {
|
||||
s.settings.Logger().InfoContext(ctx, "root user reconciliation completed successfully")
|
||||
<-s.stopC
|
||||
return nil
|
||||
}
|
||||
|
||||
s.settings.Logger().WarnContext(ctx, "root user reconciliation failed, retrying", "error", err)
|
||||
|
||||
select {
|
||||
case <-s.stopC:
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) Stop(ctx context.Context) error {
|
||||
close(s.stopC)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) reconcile(ctx context.Context) error {
|
||||
org, err := s.orgGetter.GetByName(ctx, s.config.OrgName)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
newOrg := types.NewOrganizationWithName(s.config.OrgName)
|
||||
_, 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.store.GetRootUserByOrgID(ctx, orgID)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
if existingRoot == nil {
|
||||
return s.createOrPromoteRootUser(ctx, orgID)
|
||||
}
|
||||
|
||||
return s.updateExistingRootUser(ctx, orgID, existingRoot)
|
||||
}
|
||||
|
||||
func (s *service) createOrPromoteRootUser(ctx context.Context, orgID valuer.UUID) error {
|
||||
existingUser, err := s.store.GetUserByEmailAndOrgID(ctx, s.config.Email, orgID)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
if existingUser != nil {
|
||||
oldRole := existingUser.Role
|
||||
|
||||
existingUser.PromoteToRoot()
|
||||
if err := s.module.UpdateAnyUser(ctx, orgID, existingUser); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldRole != types.RoleAdmin {
|
||||
if err := s.authz.ModifyGrant(ctx,
|
||||
orgID,
|
||||
roletypes.MustGetSigNozManagedRoleFromExistingRole(oldRole),
|
||||
roletypes.MustGetSigNozManagedRoleFromExistingRole(types.RoleAdmin),
|
||||
authtypes.MustNewSubject(authtypes.TypeableUser, existingUser.ID.StringValue(), orgID, nil),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return s.setPassword(ctx, existingUser.ID)
|
||||
}
|
||||
|
||||
// Create new root user
|
||||
newUser, err := types.NewRootUser(s.config.Email.String(), s.config.Email, orgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
factorPassword, err := types.NewFactorPassword(s.config.Password, newUser.ID.StringValue())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.module.CreateUser(ctx, newUser, user.WithFactorPassword(factorPassword))
|
||||
}
|
||||
|
||||
func (s *service) updateExistingRootUser(ctx context.Context, orgID valuer.UUID, existingRoot *types.User) error {
|
||||
existingRoot.PromoteToRoot()
|
||||
|
||||
if existingRoot.Email != s.config.Email {
|
||||
existingRoot.UpdateEmail(s.config.Email)
|
||||
if err := s.module.UpdateAnyUser(ctx, orgID, existingRoot); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return s.setPassword(ctx, existingRoot.ID)
|
||||
}
|
||||
|
||||
func (s *service) setPassword(ctx context.Context, userID valuer.UUID) error {
|
||||
password, err := s.store.GetPasswordByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
if !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
factorPassword, err := types.NewFactorPassword(s.config.Password, userID.StringValue())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.store.CreatePassword(ctx, factorPassword)
|
||||
}
|
||||
|
||||
if !password.Equals(s.config.Password) {
|
||||
if err := password.Update(s.config.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.store.UpdatePassword(ctx, password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -210,24 +210,20 @@ func (store *store) GetUsersByRoleAndOrgID(ctx context.Context, role types.Role,
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, user *types.User) error {
|
||||
_, err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewUpdate().
|
||||
func (store *store) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.User) (*types.User, error) {
|
||||
user.UpdatedAt = time.Now()
|
||||
_, err := store.sqlstore.BunDB().NewUpdate().
|
||||
Model(user).
|
||||
Column("display_name").
|
||||
Column("email").
|
||||
Column("role").
|
||||
Column("is_root").
|
||||
Column("updated_at").
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("id = ?", user.ID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "user does not exist in org: %s", orgID)
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "user with id: %s does not exist in org: %s", id, orgID)
|
||||
}
|
||||
return nil
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (store *store) ListUsersByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.GettableUser, error) {
|
||||
@@ -606,22 +602,6 @@ func (store *store) RunInTx(ctx context.Context, cb func(ctx context.Context) er
|
||||
})
|
||||
}
|
||||
|
||||
func (store *store) GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*types.User, error) {
|
||||
user := new(types.User)
|
||||
err := store.
|
||||
sqlstore.
|
||||
BunDBCtx(ctx).
|
||||
NewSelect().
|
||||
Model(user).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("is_root = ?", true).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, store.sqlstore.WrapNotFoundErrf(err, types.ErrCodeUserNotFound, "root user for org %s not found", orgID)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (store *store) ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*types.User, error) {
|
||||
users := []*types.User{}
|
||||
err := store.
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package user
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/factory"
|
||||
|
||||
type Service interface {
|
||||
factory.Service
|
||||
}
|
||||
@@ -34,9 +34,6 @@ type Module interface {
|
||||
ForgotPassword(ctx context.Context, orgID valuer.UUID, email valuer.Email, frontendBaseURL string) error
|
||||
|
||||
UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *types.User, updatedBy string) (*types.User, error)
|
||||
|
||||
// UpdateAnyUser updates a user and persists the changes to the database along with the analytics and identity deletion.
|
||||
UpdateAnyUser(ctx context.Context, orgID valuer.UUID, user *types.User) error
|
||||
DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error
|
||||
|
||||
// invite
|
||||
@@ -57,9 +54,6 @@ type Module interface {
|
||||
}
|
||||
|
||||
type Getter interface {
|
||||
// Get root user by org id.
|
||||
GetRootUserByOrgID(context.Context, valuer.UUID) (*types.User, error)
|
||||
|
||||
// Get gets the users based on the given id
|
||||
ListByOrgID(context.Context, valuer.UUID) ([]*types.User, error)
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ type APIHandlerOpts struct {
|
||||
}
|
||||
|
||||
// NewAPIHandler returns an APIHandler
|
||||
func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, error) {
|
||||
func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
|
||||
querierOpts := querier.QuerierOptions{
|
||||
Reader: opts.Reader,
|
||||
Cache: opts.Signoz.Cache,
|
||||
@@ -270,11 +270,6 @@ func NewAPIHandler(opts APIHandlerOpts, config signoz.Config) (*APIHandler, erro
|
||||
}
|
||||
}
|
||||
|
||||
// If the root user is enabled, the setup is complete
|
||||
if config.User.Root.Enabled {
|
||||
aH.SetupCompleted = true
|
||||
}
|
||||
|
||||
aH.Upgrader = &websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
|
||||
@@ -135,7 +135,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
Signoz: signoz,
|
||||
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier, signoz.Analytics),
|
||||
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
|
||||
}, config)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -167,7 +167,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewMigrateRbacToAuthzFactory(sqlstore),
|
||||
sqlmigration.NewMigratePublicDashboardsFactory(sqlstore),
|
||||
sqlmigration.NewAddAnonymousPublicDashboardTransactionFactory(sqlstore),
|
||||
sqlmigration.NewAddRootUserFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -389,8 +389,6 @@ func New(
|
||||
// Initialize all modules
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard)
|
||||
|
||||
userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, config.User.Root)
|
||||
|
||||
// Initialize all handlers for the modules
|
||||
handlers := NewHandlers(modules, providerSettings, querier, licensing, global, flagger, gateway, telemetryMetadataStore, authz)
|
||||
|
||||
@@ -440,7 +438,6 @@ func New(
|
||||
factory.NewNamedService(factory.MustNewName("statsreporter"), statsReporter),
|
||||
factory.NewNamedService(factory.MustNewName("tokenizer"), tokenizer),
|
||||
factory.NewNamedService(factory.MustNewName("authz"), authz),
|
||||
factory.NewNamedService(factory.MustNewName("user"), userService),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type addRootUser struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewAddRootUserFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("add_root_user"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
|
||||
return &addRootUser{
|
||||
sqlstore: sqlstore,
|
||||
sqlschema: sqlschema,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (migration *addRootUser) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addRootUser) Up(ctx context.Context, db *bun.DB) error {
|
||||
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("users"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
column := &sqlschema.Column{
|
||||
Name: sqlschema.ColumnName("is_root"),
|
||||
DataType: sqlschema.DataTypeBoolean,
|
||||
Nullable: false,
|
||||
}
|
||||
|
||||
sqls := migration.sqlschema.Operator().AddColumn(table, uniqueConstraints, column, false)
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *addRootUser) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -41,22 +41,6 @@ func NewOrganization(displayName string) *Organization {
|
||||
}
|
||||
}
|
||||
|
||||
func NewOrganizationWithName(name string) *Organization {
|
||||
id := valuer.GenerateUUID()
|
||||
return &Organization{
|
||||
Identifiable: Identifiable{
|
||||
ID: id,
|
||||
},
|
||||
TimeAuditable: TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: name,
|
||||
DisplayName: name,
|
||||
Key: NewOrganizationKey(id),
|
||||
}
|
||||
}
|
||||
|
||||
func NewOrganizationKey(orgID valuer.UUID) uint32 {
|
||||
hasher := fnv.New32a()
|
||||
|
||||
@@ -90,7 +74,6 @@ type TTLSetting struct {
|
||||
type OrganizationStore interface {
|
||||
Create(context.Context, *Organization) error
|
||||
Get(context.Context, valuer.UUID) (*Organization, error)
|
||||
GetByName(context.Context, string) (*Organization, error)
|
||||
GetAll(context.Context) ([]*Organization, error)
|
||||
ListByKeyRange(context.Context, uint32, uint32) ([]*Organization, error)
|
||||
Update(context.Context, *Organization) error
|
||||
|
||||
@@ -11,16 +11,15 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeUserNotFound = errors.MustNewCode("user_not_found")
|
||||
ErrCodeAmbiguousUser = errors.MustNewCode("ambiguous_user")
|
||||
ErrUserAlreadyExists = errors.MustNewCode("user_already_exists")
|
||||
ErrPasswordAlreadyExists = errors.MustNewCode("password_already_exists")
|
||||
ErrResetPasswordTokenAlreadyExists = errors.MustNewCode("reset_password_token_already_exists")
|
||||
ErrPasswordNotFound = errors.MustNewCode("password_not_found")
|
||||
ErrResetPasswordTokenNotFound = errors.MustNewCode("reset_password_token_not_found")
|
||||
ErrAPIKeyAlreadyExists = errors.MustNewCode("api_key_already_exists")
|
||||
ErrAPIKeyNotFound = errors.MustNewCode("api_key_not_found")
|
||||
ErrCodeRootUserOperationUnsupported = errors.MustNewCode("root_user_operation_unsupported")
|
||||
ErrCodeUserNotFound = errors.MustNewCode("user_not_found")
|
||||
ErrCodeAmbiguousUser = errors.MustNewCode("ambiguous_user")
|
||||
ErrUserAlreadyExists = errors.MustNewCode("user_already_exists")
|
||||
ErrPasswordAlreadyExists = errors.MustNewCode("password_already_exists")
|
||||
ErrResetPasswordTokenAlreadyExists = errors.MustNewCode("reset_password_token_already_exists")
|
||||
ErrPasswordNotFound = errors.MustNewCode("password_not_found")
|
||||
ErrResetPasswordTokenNotFound = errors.MustNewCode("reset_password_token_not_found")
|
||||
ErrAPIKeyAlreadyExists = errors.MustNewCode("api_key_already_exists")
|
||||
ErrAPIKeyNotFound = errors.MustNewCode("api_key_not_found")
|
||||
)
|
||||
|
||||
type GettableUser = User
|
||||
@@ -30,10 +29,9 @@ type User struct {
|
||||
|
||||
Identifiable
|
||||
DisplayName string `bun:"display_name" json:"displayName"`
|
||||
Email valuer.Email `bun:"email" json:"email"`
|
||||
Role Role `bun:"role" json:"role"`
|
||||
OrgID valuer.UUID `bun:"org_id" json:"orgId"`
|
||||
IsRoot bool `bun:"is_root" json:"isRoot"`
|
||||
Email valuer.Email `bun:"email,type:text" json:"email"`
|
||||
Role Role `bun:"role,type:text" json:"role"`
|
||||
OrgID valuer.UUID `bun:"org_id,type:text" json:"orgId"`
|
||||
TimeAuditable
|
||||
}
|
||||
|
||||
@@ -66,7 +64,6 @@ func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUI
|
||||
Email: email,
|
||||
Role: role,
|
||||
OrgID: orgID,
|
||||
IsRoot: false,
|
||||
TimeAuditable: TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
@@ -74,65 +71,6 @@ func NewUser(displayName string, email valuer.Email, role Role, orgID valuer.UUI
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewRootUser(displayName string, email valuer.Email, orgID valuer.UUID) (*User, error) {
|
||||
if email.IsZero() {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
|
||||
}
|
||||
|
||||
if orgID.IsZero() {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgID is required")
|
||||
}
|
||||
|
||||
return &User{
|
||||
Identifiable: Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
DisplayName: displayName,
|
||||
Email: email,
|
||||
Role: RoleAdmin,
|
||||
OrgID: orgID,
|
||||
IsRoot: true,
|
||||
TimeAuditable: TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Update applies mutable fields from the input to the user. Immutable fields
|
||||
// (email, is_root, org_id, id) are preserved. Only non-zero input fields are applied.
|
||||
func (u *User) Update(displayName string, role Role) {
|
||||
if displayName != "" {
|
||||
u.DisplayName = displayName
|
||||
}
|
||||
if role != "" {
|
||||
u.Role = role
|
||||
}
|
||||
u.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// PromoteToRoot promotes the user to a root user with admin role.
|
||||
func (u *User) PromoteToRoot() {
|
||||
u.IsRoot = true
|
||||
u.Role = RoleAdmin
|
||||
u.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// UpdateEmail updates the email of the user.
|
||||
func (u *User) UpdateEmail(email valuer.Email) {
|
||||
u.Email = email
|
||||
u.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// ErrIfRoot returns an error if the user is a root user. The caller should
|
||||
// enrich the error with the specific operation using errors.WithAdditionalf.
|
||||
func (u *User) ErrIfRoot() error {
|
||||
if u.IsRoot {
|
||||
return errors.New(errors.TypeUnsupported, ErrCodeRootUserOperationUnsupported, "this operation is not supported for the root user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTraitsFromUser(user *User) map[string]any {
|
||||
return map[string]any{
|
||||
"name": user.DisplayName,
|
||||
@@ -195,7 +133,7 @@ type UserStore interface {
|
||||
// List users by email and org ids.
|
||||
ListUsersByEmailAndOrgIDs(ctx context.Context, email valuer.Email, orgIDs []valuer.UUID) ([]*User, error)
|
||||
|
||||
UpdateUser(ctx context.Context, orgID valuer.UUID, user *User) error
|
||||
UpdateUser(ctx context.Context, orgID valuer.UUID, id string, user *User) (*User, error)
|
||||
DeleteUser(ctx context.Context, orgID string, id string) error
|
||||
|
||||
// Creates a password.
|
||||
@@ -218,9 +156,6 @@ type UserStore interface {
|
||||
|
||||
CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error)
|
||||
|
||||
// Get root user by org.
|
||||
GetRootUserByOrgID(ctx context.Context, orgID valuer.UUID) (*User, error)
|
||||
|
||||
// Transaction
|
||||
RunInTx(ctx context.Context, cb func(ctx context.Context) error) error
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logger import setup_logger
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.metrics import Metrics
|
||||
@@ -20,7 +20,7 @@ logger = setup_logger(__name__)
|
||||
def create_alert_rule(
|
||||
signoz: types.SigNoz, get_token: Callable[[str, str], str]
|
||||
) -> Callable[[dict], str]:
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
rule_ids = []
|
||||
|
||||
|
||||
@@ -11,56 +11,55 @@ from wiremock.resources.mappings import (
|
||||
WireMockMatchers,
|
||||
)
|
||||
|
||||
from fixtures import types
|
||||
from fixtures import dev, types
|
||||
from fixtures.logger import setup_logger
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
# USER_ADMIN_NAME = "admin"
|
||||
# USER_ADMIN_EMAIL = "admin@integration.test"
|
||||
# USER_ADMIN_PASSWORD = "password123Z$"
|
||||
USER_ADMIN_NAME = "admin"
|
||||
USER_ADMIN_EMAIL = "admin@integration.test"
|
||||
USER_ADMIN_PASSWORD = "password123Z$"
|
||||
|
||||
USER_EDITOR_NAME = "editor"
|
||||
USER_EDITOR_EMAIL = "editor@integration.test"
|
||||
USER_EDITOR_PASSWORD = "password123Z$"
|
||||
|
||||
|
||||
# @pytest.fixture(name="create_user_admin", scope="package")
|
||||
# def create_user_admin(
|
||||
# signoz: types.SigNoz, request: pytest.FixtureRequest, pytestconfig: pytest.Config
|
||||
# ) -> types.Operation:
|
||||
# def create() -> None:
|
||||
# response = requests.post(
|
||||
# signoz.self.host_configs["8080"].get("/api/v1/register"),
|
||||
# json={
|
||||
# "name": USER_ADMIN_NAME,
|
||||
# "orgName": "",
|
||||
# "email": USER_ADMIN_EMAIL,
|
||||
# "password": USER_ADMIN_PASSWORD,
|
||||
# },
|
||||
# timeout=5,
|
||||
# )
|
||||
@pytest.fixture(name="create_user_admin", scope="package")
|
||||
def create_user_admin(
|
||||
signoz: types.SigNoz, request: pytest.FixtureRequest, pytestconfig: pytest.Config
|
||||
) -> types.Operation:
|
||||
def create() -> None:
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/register"),
|
||||
json={
|
||||
"name": USER_ADMIN_NAME,
|
||||
"orgName": "",
|
||||
"email": USER_ADMIN_EMAIL,
|
||||
"password": USER_ADMIN_PASSWORD,
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
# assert response.status_code == HTTPStatus.OK
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
# return types.Operation(name="create_user_admin")
|
||||
return types.Operation(name="create_user_admin")
|
||||
|
||||
# def delete(_: types.Operation) -> None:
|
||||
# pass
|
||||
def delete(_: types.Operation) -> None:
|
||||
pass
|
||||
|
||||
# def restore(cache: dict) -> types.Operation:
|
||||
# return types.Operation(name=cache["name"])
|
||||
def restore(cache: dict) -> types.Operation:
|
||||
return types.Operation(name=cache["name"])
|
||||
|
||||
# return dev.wrap(
|
||||
# request,
|
||||
# pytestconfig,
|
||||
# "create_user_admin",
|
||||
# lambda: types.Operation(name=""),
|
||||
# create,
|
||||
# delete,
|
||||
# restore,
|
||||
# )
|
||||
return dev.wrap(
|
||||
request,
|
||||
pytestconfig,
|
||||
"create_user_admin",
|
||||
lambda: types.Operation(name=""),
|
||||
create,
|
||||
delete,
|
||||
restore,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="get_session_context", scope="function")
|
||||
@@ -190,7 +189,7 @@ def add_license(
|
||||
],
|
||||
)
|
||||
|
||||
access_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
access_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
|
||||
|
||||
@@ -7,7 +7,7 @@ import pytest
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
@@ -73,7 +73,7 @@ def create_cloud_integration_account(
|
||||
if created_account_id and cloud_provider_used:
|
||||
get_token = request.getfixturevalue("get_token")
|
||||
try:
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
r = _disconnect(admin_token, cloud_provider_used)
|
||||
if r.status_code != HTTPStatus.OK:
|
||||
logger.info(
|
||||
|
||||
@@ -9,7 +9,7 @@ from testcontainers.core.container import Network
|
||||
from wiremock.testing.testcontainer import WireMockContainer
|
||||
|
||||
from fixtures import dev, types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
@@ -74,9 +74,10 @@ def notification_channel(
|
||||
@pytest.fixture(name="create_webhook_notification_channel", scope="function")
|
||||
def create_webhook_notification_channel(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> Callable[[str, str, dict, bool], str]:
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# function to create notification channel
|
||||
def _create_webhook_notification_channel(
|
||||
|
||||
@@ -15,9 +15,6 @@ from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
ROOT_USER_EMAIL = "root@integration.test"
|
||||
ROOT_USER_PASSWORD = "Str0ngP@ssw0rd!"
|
||||
|
||||
|
||||
@pytest.fixture(name="signoz", scope="package")
|
||||
def signoz( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
@@ -76,9 +73,6 @@ def signoz( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
"SIGNOZ_ALERTMANAGER_SIGNOZ_POLL__INTERVAL": "5s",
|
||||
"SIGNOZ_ALERTMANAGER_SIGNOZ_ROUTE_GROUP__WAIT": "1s",
|
||||
"SIGNOZ_ALERTMANAGER_SIGNOZ_ROUTE_GROUP__INTERVAL": "5s",
|
||||
"SIGNOZ_USER_ROOT_ENABLED": True,
|
||||
"SIGNOZ_USER_ROOT_EMAIL": ROOT_USER_EMAIL,
|
||||
"SIGNOZ_USER_ROOT_PASSWORD": ROOT_USER_PASSWORD,
|
||||
}
|
||||
| sqlstore.env
|
||||
| clickhouse.env
|
||||
|
||||
@@ -7,7 +7,7 @@ import requests
|
||||
from wiremock.client import HttpMethods, Mapping, MappingRequest, MappingResponse
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
@@ -65,7 +65,7 @@ def test_webhook_notification_channel(
|
||||
time.sleep(10)
|
||||
|
||||
# Call test API for the notification channel
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = requests.post(
|
||||
url=signoz.self.host_configs["8080"].get("/api/v1/testChannel"),
|
||||
json={
|
||||
|
||||
@@ -35,6 +35,7 @@ def test_telemetry_databases_exist(signoz: types.SigNoz) -> None:
|
||||
def test_teardown(
|
||||
signoz: types.SigNoz, # pylint: disable=unused-argument
|
||||
idp: types.TestContainerIDP, # pylint: disable=unused-argument
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
migrator: types.Operation, # pylint: disable=unused-argument
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@@ -3,15 +3,16 @@ from typing import Callable
|
||||
|
||||
import requests
|
||||
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.types import SigNoz
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.types import Operation, SigNoz
|
||||
|
||||
|
||||
def test_create_and_get_domain(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Get domains which should be an empty list
|
||||
response = requests.get(
|
||||
@@ -90,9 +91,10 @@ def test_create_and_get_domain(
|
||||
|
||||
def test_create_invalid(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a domain with type saml and body for oidc, this should fail because oidcConfig is not allowed for saml
|
||||
response = requests.post(
|
||||
@@ -171,10 +173,11 @@ def test_create_invalid(
|
||||
|
||||
def test_create_invalid_role_mapping(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""Test that invalid role mappings are rejected."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create domain with invalid defaultRole
|
||||
response = requests.post(
|
||||
|
||||
@@ -6,18 +6,22 @@ import requests
|
||||
from selenium import webdriver
|
||||
from wiremock.resources.mappings import Mapping
|
||||
|
||||
from fixtures.auth import add_license
|
||||
from fixtures.auth import (
|
||||
USER_ADMIN_EMAIL,
|
||||
USER_ADMIN_PASSWORD,
|
||||
add_license,
|
||||
)
|
||||
from fixtures.idputils import (
|
||||
get_saml_domain,
|
||||
get_user_by_email,
|
||||
perform_saml_login,
|
||||
)
|
||||
from fixtures.types import SigNoz, TestContainerDocker, TestContainerIDP
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.types import Operation, SigNoz, TestContainerDocker, TestContainerIDP
|
||||
|
||||
|
||||
def test_apply_license(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[TestContainerDocker, List[Mapping]], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
@@ -30,6 +34,7 @@ def test_create_auth_domain(
|
||||
create_saml_client: Callable[[str, str], None],
|
||||
update_saml_client_attributes: Callable[[str, Dict[str, Any]], None],
|
||||
get_saml_settings: Callable[[], dict],
|
||||
create_user_admin: Callable[[], None], # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
# Create a saml client in the idp.
|
||||
@@ -39,7 +44,7 @@ def test_create_auth_domain(
|
||||
settings = get_saml_settings()
|
||||
|
||||
# Create a auth domain in signoz.
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
@@ -122,7 +127,7 @@ def test_saml_authn(
|
||||
driver.get(url)
|
||||
idp_login("viewer@saml.integration.test", "password")
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Assert that the user was created in signoz.
|
||||
response = requests.get(
|
||||
@@ -173,7 +178,7 @@ def test_idp_initiated_saml_authn(
|
||||
driver.get(idp_initiated_login_url)
|
||||
idp_login("viewer.idp.initiated@saml.integration.test", "password")
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Assert that the user was created in signoz.
|
||||
response = requests.get(
|
||||
@@ -203,7 +208,7 @@ def test_saml_update_domain_with_group_mappings(
|
||||
get_token: Callable[[str, str], str],
|
||||
get_saml_settings: Callable[[], dict],
|
||||
) -> None:
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
domain = get_saml_domain(signoz, admin_token)
|
||||
settings = get_saml_settings()
|
||||
|
||||
@@ -261,7 +266,7 @@ def test_saml_role_mapping_single_group_admin(
|
||||
signoz, driver, get_session_context, idp_login, email, "password"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -287,7 +292,7 @@ def test_saml_role_mapping_single_group_editor(
|
||||
signoz, driver, get_session_context, idp_login, email, "password"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -317,7 +322,7 @@ def test_saml_role_mapping_multiple_groups_highest_wins(
|
||||
signoz, driver, get_session_context, idp_login, email, "password"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -344,7 +349,7 @@ def test_saml_role_mapping_explicit_viewer_group(
|
||||
signoz, driver, get_session_context, idp_login, email, "password"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -370,7 +375,7 @@ def test_saml_role_mapping_unmapped_group_uses_default(
|
||||
signoz, driver, get_session_context, idp_login, email, "password"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -385,7 +390,7 @@ def test_saml_update_domain_with_use_role_claim(
|
||||
"""
|
||||
Updates SAML domain to enable useRoleAttribute (direct role attribute).
|
||||
"""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
domain = get_saml_domain(signoz, admin_token)
|
||||
settings = get_saml_settings()
|
||||
|
||||
@@ -447,7 +452,7 @@ def test_saml_role_mapping_role_claim_takes_precedence(
|
||||
signoz, driver, get_session_context, idp_login, email, "password"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -477,7 +482,7 @@ def test_saml_role_mapping_invalid_role_claim_fallback(
|
||||
signoz, driver, get_session_context, idp_login, email, "password"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -507,7 +512,7 @@ def test_saml_role_mapping_case_insensitive(
|
||||
signoz, driver, get_session_context, idp_login, email, "password"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -532,7 +537,7 @@ def test_saml_name_mapping(
|
||||
signoz, driver, get_session_context, idp_login, email, "password"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -560,7 +565,7 @@ def test_saml_empty_name_fallback(
|
||||
signoz, driver, get_session_context, idp_login, email, "password"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
|
||||
@@ -6,18 +6,22 @@ import requests
|
||||
from selenium import webdriver
|
||||
from wiremock.resources.mappings import Mapping
|
||||
|
||||
from fixtures.auth import add_license
|
||||
from fixtures.auth import (
|
||||
USER_ADMIN_EMAIL,
|
||||
USER_ADMIN_PASSWORD,
|
||||
add_license,
|
||||
)
|
||||
from fixtures.idputils import (
|
||||
get_oidc_domain,
|
||||
get_user_by_email,
|
||||
perform_oidc_login,
|
||||
)
|
||||
from fixtures.types import SigNoz, TestContainerDocker, TestContainerIDP
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.types import Operation, SigNoz, TestContainerDocker, TestContainerIDP
|
||||
|
||||
|
||||
def test_apply_license(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[TestContainerDocker, List[Mapping]], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
@@ -32,6 +36,7 @@ def test_create_auth_domain(
|
||||
idp: TestContainerIDP, # pylint: disable=unused-argument
|
||||
create_oidc_client: Callable[[str, str], None],
|
||||
get_oidc_settings: Callable[[], dict],
|
||||
create_user_admin: Callable[[], None], # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""
|
||||
@@ -45,7 +50,7 @@ def test_create_auth_domain(
|
||||
settings = get_oidc_settings(client_id)
|
||||
|
||||
# Create a auth domain in signoz.
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/domains"),
|
||||
@@ -104,7 +109,7 @@ def test_oidc_authn(
|
||||
driver.get(actual_url)
|
||||
idp_login("viewer@oidc.integration.test", "password123")
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Assert that the user was created in signoz.
|
||||
response = requests.get(
|
||||
@@ -138,7 +143,7 @@ def test_oidc_update_domain_with_group_mappings(
|
||||
"""
|
||||
Updates OIDC domain to add role mapping with group mappings and claim mapping.
|
||||
"""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
domain = get_oidc_domain(signoz, admin_token)
|
||||
client_id = f"oidc.integration.test.{signoz.self.host_configs['8080'].address}:{signoz.self.host_configs['8080'].port}"
|
||||
settings = get_oidc_settings(client_id)
|
||||
@@ -199,7 +204,7 @@ def test_oidc_role_mapping_single_group_admin(
|
||||
signoz, idp, driver, get_session_context, idp_login, email, "password123"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -225,7 +230,7 @@ def test_oidc_role_mapping_single_group_editor(
|
||||
signoz, idp, driver, get_session_context, idp_login, email, "password123"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -255,7 +260,7 @@ def test_oidc_role_mapping_multiple_groups_highest_wins(
|
||||
signoz, idp, driver, get_session_context, idp_login, email, "password123"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -282,7 +287,7 @@ def test_oidc_role_mapping_explicit_viewer_group(
|
||||
signoz, idp, driver, get_session_context, idp_login, email, "password123"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -308,7 +313,7 @@ def test_oidc_role_mapping_unmapped_group_uses_default(
|
||||
signoz, idp, driver, get_session_context, idp_login, email, "password123"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -324,7 +329,7 @@ def test_oidc_update_domain_with_use_role_claim(
|
||||
"""
|
||||
Updates OIDC domain to enable useRoleClaim.
|
||||
"""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
domain = get_oidc_domain(signoz, admin_token)
|
||||
client_id = f"oidc.integration.test.{signoz.self.host_configs['8080'].address}:{signoz.self.host_configs['8080'].port}"
|
||||
settings = get_oidc_settings(client_id)
|
||||
@@ -388,7 +393,7 @@ def test_oidc_role_mapping_role_claim_takes_precedence(
|
||||
signoz, idp, driver, get_session_context, idp_login, email, "password123"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -420,7 +425,7 @@ def test_oidc_role_mapping_invalid_role_claim_fallback(
|
||||
signoz, idp, driver, get_session_context, idp_login, email, "password123"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -450,7 +455,7 @@ def test_oidc_role_mapping_case_insensitive(
|
||||
signoz, idp, driver, get_session_context, idp_login, email, "password123"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
found_user = get_user_by_email(signoz, admin_token, email)
|
||||
|
||||
assert found_user is not None
|
||||
@@ -476,7 +481,7 @@ def test_oidc_name_mapping(
|
||||
signoz, idp, driver, get_session_context, idp_login, email, "password123"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
@@ -512,7 +517,7 @@ def test_oidc_empty_name_uses_fallback(
|
||||
signoz, idp, driver, get_session_context, idp_login, email, "password123"
|
||||
)
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
|
||||
@@ -11,21 +11,21 @@ from wiremock.client import (
|
||||
)
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import add_license
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
||||
from fixtures.logger import setup_logger
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def test_generate_connection_params(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[types.TestContainerDocker, list], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test to generate connection parameters for AWS SigNoz cloud integration."""
|
||||
# Get authentication token for admin user
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
add_license(signoz, make_http_mocks, get_token)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Callable
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
@@ -12,12 +12,13 @@ logger = setup_logger(__name__)
|
||||
|
||||
def test_generate_connection_url(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test to generate connection URL for AWS CloudFormation stack deployment."""
|
||||
|
||||
# Get authentication token for admin user
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
cloud_provider = "aws"
|
||||
endpoint = (
|
||||
f"/api/v1/cloud-integrations/{cloud_provider}/accounts/generate-connection-url"
|
||||
@@ -100,11 +101,12 @@ def test_generate_connection_url(
|
||||
|
||||
def test_generate_connection_url_unsupported_provider(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test that unsupported cloud providers return an error."""
|
||||
# Get authentication token for admin user
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
# Try with GCP (unsupported)
|
||||
cloud_provider = "gcp"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Callable
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.cloudintegrationsutils import simulate_agent_checkin
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
@@ -14,10 +14,11 @@ logger = setup_logger(__name__)
|
||||
|
||||
def test_list_connected_accounts_empty(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test listing connected accounts when there are none."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
cloud_provider = "aws"
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/accounts"
|
||||
|
||||
@@ -42,11 +43,12 @@ def test_list_connected_accounts_empty(
|
||||
|
||||
def test_list_connected_accounts_with_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
) -> None:
|
||||
"""Test listing connected accounts after creating one."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a test account
|
||||
cloud_provider = "aws"
|
||||
@@ -86,11 +88,12 @@ def test_list_connected_accounts_with_account(
|
||||
|
||||
def test_get_account_status(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
) -> None:
|
||||
"""Test getting the status of a specific account."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
# Create a test account (no check-in needed for status check)
|
||||
cloud_provider = "aws"
|
||||
account_data = create_cloud_integration_account(admin_token, cloud_provider)
|
||||
@@ -120,10 +123,11 @@ def test_get_account_status(
|
||||
|
||||
def test_get_account_status_not_found(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test getting status for a non-existent account."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
cloud_provider = "aws"
|
||||
fake_account_id = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
@@ -143,11 +147,12 @@ def test_get_account_status_not_found(
|
||||
|
||||
def test_update_account_config(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
) -> None:
|
||||
"""Test updating account configuration."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a test account
|
||||
cloud_provider = "aws"
|
||||
@@ -207,11 +212,12 @@ def test_update_account_config(
|
||||
|
||||
def test_disconnect_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
) -> None:
|
||||
"""Test disconnecting an account."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a test account
|
||||
cloud_provider = "aws"
|
||||
@@ -261,10 +267,11 @@ def test_disconnect_account(
|
||||
|
||||
def test_disconnect_account_not_found(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test disconnecting a non-existent account."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
cloud_provider = "aws"
|
||||
fake_account_id = "00000000-0000-0000-0000-000000000000"
|
||||
@@ -284,11 +291,12 @@ def test_disconnect_account_not_found(
|
||||
|
||||
def test_list_accounts_unsupported_provider(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test listing accounts for an unsupported cloud provider."""
|
||||
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
cloud_provider = "gcp" # Unsupported provider
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/accounts"
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Callable
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.cloudintegrationsutils import simulate_agent_checkin
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
@@ -14,10 +14,11 @@ logger = setup_logger(__name__)
|
||||
|
||||
def test_list_services_without_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test listing available services without specifying an account."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
cloud_provider = "aws"
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services"
|
||||
@@ -47,11 +48,12 @@ def test_list_services_without_account(
|
||||
|
||||
def test_list_services_with_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
) -> None:
|
||||
"""Test listing services for a specific connected account."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a test account and do check-in
|
||||
cloud_provider = "aws"
|
||||
@@ -91,10 +93,11 @@ def test_list_services_with_account(
|
||||
|
||||
def test_get_service_details_without_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test getting service details without specifying an account."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
cloud_provider = "aws"
|
||||
# First get the list of services to get a valid service ID
|
||||
@@ -136,11 +139,12 @@ def test_get_service_details_without_account(
|
||||
|
||||
def test_get_service_details_with_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
) -> None:
|
||||
"""Test getting service details for a specific connected account."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a test account and do check-in
|
||||
cloud_provider = "aws"
|
||||
@@ -191,10 +195,11 @@ def test_get_service_details_with_account(
|
||||
|
||||
def test_get_service_details_invalid_service(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test getting details for a non-existent service."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
cloud_provider = "aws"
|
||||
fake_service_id = "non-existent-service"
|
||||
@@ -213,10 +218,11 @@ def test_get_service_details_invalid_service(
|
||||
|
||||
def test_list_services_unsupported_provider(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test listing services for an unsupported cloud provider."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
cloud_provider = "gcp" # Unsupported provider
|
||||
endpoint = f"/api/v1/cloud-integrations/{cloud_provider}/services"
|
||||
@@ -234,11 +240,12 @@ def test_list_services_unsupported_provider(
|
||||
|
||||
def test_update_service_config(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
) -> None:
|
||||
"""Test updating service configuration for a connected account."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a test account and do check-in
|
||||
cloud_provider = "aws"
|
||||
@@ -299,10 +306,11 @@ def test_update_service_config(
|
||||
|
||||
def test_update_service_config_without_account(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""Test updating service config without a connected account should fail."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
cloud_provider = "aws"
|
||||
|
||||
@@ -344,11 +352,12 @@ def test_update_service_config_without_account(
|
||||
|
||||
def test_update_service_config_invalid_service(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
) -> None:
|
||||
"""Test updating config for a non-existent service should fail."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a test account and do check-in
|
||||
cloud_provider = "aws"
|
||||
@@ -387,11 +396,12 @@ def test_update_service_config_invalid_service(
|
||||
|
||||
def test_update_service_config_disable_service(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
create_cloud_integration_account: Callable,
|
||||
) -> None:
|
||||
"""Test disabling a service by updating config with enabled=false."""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a test account and do check-in
|
||||
cloud_provider = "aws"
|
||||
|
||||
@@ -4,16 +4,16 @@ from typing import Callable, List
|
||||
import requests
|
||||
from wiremock.resources.mappings import Mapping
|
||||
|
||||
from fixtures.auth import add_license
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.types import SigNoz, TestContainerDocker
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
||||
from fixtures.types import Operation, SigNoz, TestContainerDocker
|
||||
|
||||
|
||||
def test_create_and_delete_dashboard_without_license(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/dashboards"),
|
||||
@@ -38,6 +38,7 @@ def test_create_and_delete_dashboard_without_license(
|
||||
|
||||
def test_apply_license(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[TestContainerDocker, List[Mapping]], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
@@ -49,9 +50,10 @@ def test_apply_license(
|
||||
|
||||
def test_create_and_delete_dashboard_with_license(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/dashboards"),
|
||||
|
||||
@@ -6,13 +6,13 @@ import requests
|
||||
from sqlalchemy import sql
|
||||
from wiremock.resources.mappings import Mapping
|
||||
|
||||
from fixtures.auth import add_license
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.types import SigNoz, TestContainerDocker
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD, add_license
|
||||
from fixtures.types import Operation, SigNoz, TestContainerDocker
|
||||
|
||||
|
||||
def test_apply_license(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
make_http_mocks: Callable[[TestContainerDocker, List[Mapping]], None],
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
@@ -24,9 +24,10 @@ def test_apply_license(
|
||||
|
||||
def test_create_and_get_public_dashboard(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/dashboards"),
|
||||
@@ -71,9 +72,10 @@ def test_create_and_get_public_dashboard(
|
||||
|
||||
def test_public_dashboard_widget_query_range(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
dashboard_req = {
|
||||
"title": "Test Widget Query Range Dashboard",
|
||||
@@ -194,12 +196,13 @@ def test_public_dashboard_widget_query_range(
|
||||
def test_anonymous_role_has_public_dashboard_permission(
|
||||
request: pytest.FixtureRequest,
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
"""
|
||||
Verify that the signoz-anonymous role has the public-dashboard/* permission.
|
||||
"""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Get the roles to find the org_id
|
||||
response = requests.get(
|
||||
|
||||
@@ -11,11 +11,12 @@ from typing import Callable
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
|
||||
|
||||
def test_create_logs_pipeline_success(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""
|
||||
@@ -28,7 +29,7 @@ def test_create_logs_pipeline_success(
|
||||
3. Verify the response contains version information
|
||||
4. Verify the pipeline is returned in the response
|
||||
"""
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
pipeline_payload = {
|
||||
"pipelines": [
|
||||
@@ -104,6 +105,7 @@ def test_create_logs_pipeline_success(
|
||||
|
||||
def test_list_logs_pipelines_success(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""
|
||||
@@ -115,7 +117,7 @@ def test_list_logs_pipelines_success(
|
||||
2. List all pipelines and verify the created pipeline is present
|
||||
3. Verify the response structure
|
||||
"""
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a pipeline first
|
||||
create_payload = {
|
||||
@@ -192,6 +194,7 @@ def test_list_logs_pipelines_success(
|
||||
|
||||
def test_list_logs_pipelines_by_version(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""
|
||||
@@ -204,7 +207,7 @@ def test_list_logs_pipelines_by_version(
|
||||
3. List pipelines by version 1 and verify it returns the original
|
||||
4. List pipelines by version 2 and verify it returns the updated version
|
||||
"""
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create version 1
|
||||
v1_payload = {
|
||||
@@ -353,6 +356,7 @@ def test_list_logs_pipelines_by_version(
|
||||
|
||||
def test_preview_logs_pipelines_success(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""
|
||||
@@ -364,7 +368,7 @@ def test_preview_logs_pipelines_success(
|
||||
2. Verify the preview processes logs correctly
|
||||
3. Verify the response contains processed logs
|
||||
"""
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
preview_payload = {
|
||||
"pipelines": [
|
||||
@@ -444,6 +448,7 @@ def test_preview_logs_pipelines_success(
|
||||
|
||||
def test_create_multiple_pipelines_success(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""
|
||||
@@ -455,7 +460,7 @@ def test_create_multiple_pipelines_success(
|
||||
2. Verify all pipelines are created
|
||||
3. Verify pipelines are ordered correctly
|
||||
"""
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
multi_pipeline_payload = {
|
||||
"pipelines": [
|
||||
@@ -557,6 +562,7 @@ def test_create_multiple_pipelines_success(
|
||||
|
||||
def test_delete_all_pipelines_success(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""
|
||||
@@ -569,7 +575,7 @@ def test_delete_all_pipelines_success(
|
||||
3. Verify pipelines are deleted
|
||||
4. Verify new version is created
|
||||
"""
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Create a pipeline first
|
||||
create_payload = {
|
||||
|
||||
@@ -5,102 +5,100 @@ import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.logger import setup_logger
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_EDITOR_EMAIL, USER_EDITOR_PASSWORD, USER_EDITOR_NAME
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
# def test_register_with_invalid_input(signoz: types.SigNoz) -> None:
|
||||
# """
|
||||
# Test the register endpoint with invalid input.
|
||||
# 1. Invalid Password
|
||||
# 2. Invalid Email
|
||||
# """
|
||||
# response = requests.post(
|
||||
# signoz.self.host_configs["8080"].get("/api/v1/register"),
|
||||
# json={
|
||||
# "name": "admin",
|
||||
# "orgId": "",
|
||||
# "orgName": "integration.test",
|
||||
# "email": "admin@integration.test",
|
||||
# "password": "password", # invalid password
|
||||
# },
|
||||
# timeout=2,
|
||||
# )
|
||||
def test_register_with_invalid_input(signoz: types.SigNoz) -> None:
|
||||
"""
|
||||
Test the register endpoint with invalid input.
|
||||
1. Invalid Password
|
||||
2. Invalid Email
|
||||
"""
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/register"),
|
||||
json={
|
||||
"name": "admin",
|
||||
"orgId": "",
|
||||
"orgName": "integration.test",
|
||||
"email": "admin@integration.test",
|
||||
"password": "password", # invalid password
|
||||
},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
# assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
|
||||
# response = requests.post(
|
||||
# signoz.self.host_configs["8080"].get("/api/v1/register"),
|
||||
# json={
|
||||
# "name": "admin",
|
||||
# "orgId": "",
|
||||
# "orgName": "integration.test",
|
||||
# "email": "admin", # invalid email
|
||||
# "password": "password123Z$",
|
||||
# },
|
||||
# timeout=2,
|
||||
# )
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/register"),
|
||||
json={
|
||||
"name": "admin",
|
||||
"orgId": "",
|
||||
"orgName": "integration.test",
|
||||
"email": "admin", # invalid email
|
||||
"password": "password123Z$",
|
||||
},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
# assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
|
||||
|
||||
# def test_register(signoz: types.SigNoz, get_token: Callable[[str, str], str]) -> None:
|
||||
# response = requests.get(
|
||||
# signoz.self.host_configs["8080"].get("/api/v1/version"), timeout=2
|
||||
# )
|
||||
def test_register(signoz: types.SigNoz, get_token: Callable[[str, str], str]) -> None:
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/version"), timeout=2
|
||||
)
|
||||
|
||||
# assert response.status_code == HTTPStatus.OK
|
||||
# assert response.json()["setupCompleted"] is False
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["setupCompleted"] is False
|
||||
|
||||
# response = requests.post(
|
||||
# signoz.self.host_configs["8080"].get("/api/v1/register"),
|
||||
# json={
|
||||
# "name": "admin",
|
||||
# "orgId": "",
|
||||
# "orgName": "integration.test",
|
||||
# "email": "admin@integration.test",
|
||||
# "password": "password123Z$",
|
||||
# },
|
||||
# timeout=2,
|
||||
# )
|
||||
# assert response.status_code == HTTPStatus.OK
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/register"),
|
||||
json={
|
||||
"name": "admin",
|
||||
"orgId": "",
|
||||
"orgName": "integration.test",
|
||||
"email": "admin@integration.test",
|
||||
"password": "password123Z$",
|
||||
},
|
||||
timeout=2,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
# response = requests.get(
|
||||
# signoz.self.host_configs["8080"].get("/api/v1/version"), timeout=2
|
||||
# )
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/version"), timeout=2
|
||||
)
|
||||
|
||||
# assert response.status_code == HTTPStatus.OK
|
||||
# assert response.json()["setupCompleted"] is True
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["setupCompleted"] is True
|
||||
|
||||
# admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
# response = requests.get(
|
||||
# signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||
# timeout=2,
|
||||
# headers={"Authorization": f"Bearer {admin_token}"},
|
||||
# )
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
)
|
||||
|
||||
# assert response.status_code == HTTPStatus.OK
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
# user_response = response.json()["data"]
|
||||
# found_user = next(
|
||||
# (user for user in user_response if user["email"] == "admin@integration.test"),
|
||||
# None,
|
||||
# )
|
||||
user_response = response.json()["data"]
|
||||
found_user = next(
|
||||
(user for user in user_response if user["email"] == "admin@integration.test"),
|
||||
None,
|
||||
)
|
||||
|
||||
# assert found_user is not None
|
||||
# assert found_user["role"] == "ADMIN"
|
||||
assert found_user is not None
|
||||
assert found_user["role"] == "ADMIN"
|
||||
|
||||
# response = requests.get(
|
||||
# signoz.self.host_configs["8080"].get(f"/api/v1/user/{found_user['id']}"),
|
||||
# timeout=2,
|
||||
# headers={"Authorization": f"Bearer {admin_token}"},
|
||||
# )
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get(f"/api/v1/user/{found_user["id"]}"),
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
)
|
||||
|
||||
# assert response.status_code == HTTPStatus.OK
|
||||
# assert response.json()["data"]["role"] == "ADMIN"
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["data"]["role"] == "ADMIN"
|
||||
|
||||
|
||||
def test_invite_and_register(
|
||||
@@ -109,10 +107,10 @@ def test_invite_and_register(
|
||||
# Generate an invite token for the editor user
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
json={"email": USER_EDITOR_EMAIL, "role": "EDITOR", "name": USER_EDITOR_NAME},
|
||||
json={"email": "editor@integration.test", "role": "EDITOR", "name": "editor"},
|
||||
timeout=2,
|
||||
headers={
|
||||
"Authorization": f"Bearer {get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)}"
|
||||
"Authorization": f"Bearer {get_token("admin@integration.test", "password123Z$")}"
|
||||
},
|
||||
)
|
||||
|
||||
@@ -122,7 +120,7 @@ def test_invite_and_register(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
timeout=2,
|
||||
headers={
|
||||
"Authorization": f"Bearer {get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)}"
|
||||
"Authorization": f"Bearer {get_token("admin@integration.test", "password123Z$")}"
|
||||
},
|
||||
)
|
||||
|
||||
@@ -131,7 +129,7 @@ def test_invite_and_register(
|
||||
(
|
||||
invite
|
||||
for invite in invite_response
|
||||
if invite["email"] == USER_EDITOR_EMAIL
|
||||
if invite["email"] == "editor@integration.test"
|
||||
),
|
||||
None,
|
||||
)
|
||||
@@ -140,8 +138,8 @@ def test_invite_and_register(
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite/accept"),
|
||||
json={
|
||||
"password": USER_EDITOR_PASSWORD,
|
||||
"displayName": USER_EDITOR_NAME,
|
||||
"password": "password123Z$",
|
||||
"displayName": "editor",
|
||||
"token": f"{found_invite['token']}",
|
||||
},
|
||||
timeout=2,
|
||||
@@ -161,7 +159,7 @@ def test_invite_and_register(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||
timeout=2,
|
||||
headers={
|
||||
"Authorization": f"Bearer {get_token(USER_EDITOR_EMAIL, USER_EDITOR_PASSWORD)}"
|
||||
"Authorization": f"Bearer {get_token("editor@integration.test", "password123Z$")}"
|
||||
},
|
||||
)
|
||||
|
||||
@@ -172,7 +170,7 @@ def test_invite_and_register(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||
timeout=2,
|
||||
headers={
|
||||
"Authorization": f"Bearer {get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)}"
|
||||
"Authorization": f"Bearer {get_token("admin@integration.test", "password123Z$")}"
|
||||
},
|
||||
)
|
||||
|
||||
@@ -180,20 +178,20 @@ def test_invite_and_register(
|
||||
|
||||
user_response = response.json()["data"]
|
||||
found_user = next(
|
||||
(user for user in user_response if user["email"] == USER_EDITOR_EMAIL),
|
||||
(user for user in user_response if user["email"] == "editor@integration.test"),
|
||||
None,
|
||||
)
|
||||
|
||||
assert found_user is not None
|
||||
assert found_user["role"] == "EDITOR"
|
||||
assert found_user["displayName"] == USER_EDITOR_NAME
|
||||
assert found_user["email"] == USER_EDITOR_EMAIL
|
||||
assert found_user["displayName"] == "editor"
|
||||
assert found_user["email"] == "editor@integration.test"
|
||||
|
||||
|
||||
def test_revoke_invite_and_register(
|
||||
signoz: types.SigNoz, get_token: Callable[[str, str], str]
|
||||
) -> None:
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token("admin@integration.test", "password123Z$")
|
||||
# Generate an invite token for the viewer user
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
@@ -208,7 +206,7 @@ def test_revoke_invite_and_register(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/invite"),
|
||||
timeout=2,
|
||||
headers={
|
||||
"Authorization": f"Bearer {get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)}"
|
||||
"Authorization": f"Bearer {get_token("admin@integration.test", "password123Z$")}"
|
||||
},
|
||||
)
|
||||
|
||||
@@ -236,7 +234,7 @@ def test_revoke_invite_and_register(
|
||||
json={
|
||||
"password": "password123Z$",
|
||||
"displayName": "viewer",
|
||||
"token": f"{found_invite['token']}",
|
||||
"token": f"{found_invite["token"]}",
|
||||
},
|
||||
timeout=2,
|
||||
)
|
||||
@@ -247,7 +245,7 @@ def test_revoke_invite_and_register(
|
||||
def test_self_access(
|
||||
signoz: types.SigNoz, get_token: Callable[[str, str], str]
|
||||
) -> None:
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||
@@ -259,7 +257,7 @@ def test_self_access(
|
||||
|
||||
user_response = response.json()["data"]
|
||||
found_user = next(
|
||||
(user for user in user_response if user["email"] == USER_EDITOR_EMAIL),
|
||||
(user for user in user_response if user["email"] == "editor@integration.test"),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from wiremock.client import (
|
||||
)
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
|
||||
|
||||
def test_apply_license(
|
||||
@@ -57,7 +56,7 @@ def test_apply_license(
|
||||
],
|
||||
)
|
||||
|
||||
access_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
access_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
response = requests.post(
|
||||
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
|
||||
@@ -120,7 +119,7 @@ def test_refresh_license(
|
||||
],
|
||||
)
|
||||
|
||||
access_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
access_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
response = requests.put(
|
||||
url=signoz.self.host_configs["8080"].get("/api/v3/licenses"),
|
||||
@@ -177,7 +176,7 @@ def test_license_checkout(
|
||||
],
|
||||
)
|
||||
|
||||
access_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
access_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
response = requests.post(
|
||||
url=signoz.self.host_configs["8080"].get("/api/v1/checkout"),
|
||||
@@ -228,7 +227,7 @@ def test_license_portal(
|
||||
],
|
||||
)
|
||||
|
||||
access_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
access_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
response = requests.post(
|
||||
url=signoz.self.host_configs["8080"].get("/api/v1/portal"),
|
||||
|
||||
@@ -4,11 +4,10 @@ from typing import Callable
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
|
||||
|
||||
def test_api_key(signoz: types.SigNoz, get_token: Callable[[str, str], str]) -> None:
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
response = requests.post(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/pats"),
|
||||
@@ -29,7 +28,7 @@ def test_api_key(signoz: types.SigNoz, get_token: Callable[[str, str], str]) ->
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/user"),
|
||||
timeout=2,
|
||||
headers={"SIGNOZ-API-KEY": f"{pat_response['data']['token']}"},
|
||||
headers={"SIGNOZ-API-KEY": f"{pat_response["data"]["token"]}"},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
@@ -39,14 +38,14 @@ def test_api_key(signoz: types.SigNoz, get_token: Callable[[str, str], str]) ->
|
||||
(
|
||||
user
|
||||
for user in user_response["data"]
|
||||
if user["email"] == ROOT_USER_EMAIL
|
||||
if user["email"] == "admin@integration.test"
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/pats"),
|
||||
headers={"SIGNOZ-API-KEY": f"{pat_response['data']['token']}"},
|
||||
headers={"SIGNOZ-API-KEY": f"{pat_response["data"]["token"]}"},
|
||||
timeout=2,
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from sqlalchemy import sql
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.logger import setup_logger
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
@@ -14,7 +13,7 @@ logger = setup_logger(__name__)
|
||||
def test_change_password(
|
||||
signoz: types.SigNoz, get_token: Callable[[str, str], str]
|
||||
) -> None:
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
# Create another admin user
|
||||
response = requests.post(
|
||||
@@ -131,7 +130,7 @@ def test_change_password(
|
||||
def test_reset_password(
|
||||
signoz: types.SigNoz, get_token: Callable[[str, str], str]
|
||||
) -> None:
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
# Get the user id for admin+password@integration.test
|
||||
response = requests.get(
|
||||
@@ -189,7 +188,7 @@ def test_reset_password(
|
||||
def test_reset_password_with_no_password(
|
||||
signoz: types.SigNoz, get_token: Callable[[str, str], str]
|
||||
) -> None:
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
# Get the user id for admin+password@integration.test
|
||||
response = requests.get(
|
||||
@@ -254,7 +253,7 @@ def test_forgot_password_returns_204_for_nonexistent_email(
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v2/sessions/context"),
|
||||
params={
|
||||
"email": ROOT_USER_EMAIL,
|
||||
"email": "admin@integration.test",
|
||||
"ref": f"{signoz.self.host_configs['8080'].base()}",
|
||||
},
|
||||
timeout=5,
|
||||
@@ -287,7 +286,7 @@ def test_forgot_password_creates_reset_token(
|
||||
3. Use the token to reset password
|
||||
4. Verify user can login with new password
|
||||
"""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
# Create a user specifically for testing forgot password
|
||||
response = requests.post(
|
||||
@@ -419,7 +418,7 @@ def test_reset_password_with_expired_token(
|
||||
"""
|
||||
Test that resetting password with an expired token fails.
|
||||
"""
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
# Get user ID for the forgot@integration.test user
|
||||
response = requests.get(
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Callable, Tuple
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
|
||||
|
||||
def test_change_role(
|
||||
@@ -12,7 +11,7 @@ def test_change_role(
|
||||
get_token: Callable[[str, str], str],
|
||||
get_tokens: Callable[[str, str], Tuple[str, str]],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token("admin@integration.test", "password123Z$")
|
||||
|
||||
# Create a new user as VIEWER
|
||||
response = requests.post(
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Callable
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
@@ -12,9 +12,10 @@ logger = setup_logger(__name__)
|
||||
|
||||
def test_get_user_preference(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/user/preferences"),
|
||||
@@ -28,9 +29,10 @@ def test_get_user_preference(
|
||||
|
||||
def test_get_set_user_preference_by_name(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# preference does not exist
|
||||
response = requests.get(
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Callable
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
@@ -12,9 +12,10 @@ logger = setup_logger(__name__)
|
||||
|
||||
def test_get_org_preference(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/org/preferences"),
|
||||
@@ -28,9 +29,10 @@ def test_get_org_preference(
|
||||
|
||||
def test_get_set_org_preference_by_name(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: types.Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# preference does not exist
|
||||
response = requests.get(
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.querier import (
|
||||
assert_minutely_bucket_values,
|
||||
@@ -19,6 +19,7 @@ from src.querier.util import assert_identical_query_response
|
||||
|
||||
def test_logs_list(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -83,7 +84,7 @@ def test_logs_list(
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Query Logs for the last 10 seconds and check if the logs are returned in the correct order
|
||||
response = requests.post(
|
||||
@@ -400,6 +401,7 @@ def test_logs_list(
|
||||
|
||||
def test_logs_list_with_corrupt_data(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -468,7 +470,7 @@ def test_logs_list_with_corrupt_data(
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Query Logs for the last 10 seconds and check if the logs are returned in the correct order
|
||||
response = requests.post(
|
||||
@@ -581,6 +583,7 @@ def test_logs_list_with_corrupt_data(
|
||||
)
|
||||
def test_logs_list_with_order_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
order_by_context: str,
|
||||
@@ -610,7 +613,7 @@ def test_logs_list_with_order_by(
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
query = {
|
||||
"type": "builder_query",
|
||||
@@ -682,6 +685,7 @@ def test_logs_list_with_order_by(
|
||||
|
||||
def test_logs_time_series_count(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -784,7 +788,7 @@ def test_logs_time_series_count(
|
||||
)
|
||||
insert_logs(logs)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# count() of all logs for the last 5 minutes
|
||||
response = requests.post(
|
||||
@@ -1222,6 +1226,7 @@ def test_logs_time_series_count(
|
||||
|
||||
def test_datatype_collision(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -1327,7 +1332,7 @@ def test_datatype_collision(
|
||||
|
||||
insert_logs(logs)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# count() of all logs for the where severity_number > '7'
|
||||
response = requests.post(
|
||||
@@ -1656,6 +1661,7 @@ def test_datatype_collision(
|
||||
|
||||
def test_logs_fill_gaps(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -1681,7 +1687,7 @@ def test_logs_fill_gaps(
|
||||
]
|
||||
insert_logs(logs)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -1741,6 +1747,7 @@ def test_logs_fill_gaps(
|
||||
|
||||
def test_logs_fill_gaps_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -1766,7 +1773,7 @@ def test_logs_fill_gaps_with_group_by(
|
||||
]
|
||||
insert_logs(logs)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -1841,6 +1848,7 @@ def test_logs_fill_gaps_with_group_by(
|
||||
|
||||
def test_logs_fill_gaps_formula(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -1866,7 +1874,7 @@ def test_logs_fill_gaps_formula(
|
||||
]
|
||||
insert_logs(logs)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -1947,6 +1955,7 @@ def test_logs_fill_gaps_formula(
|
||||
|
||||
def test_logs_fill_gaps_formula_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -1972,7 +1981,7 @@ def test_logs_fill_gaps_formula_with_group_by(
|
||||
]
|
||||
insert_logs(logs)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -2074,6 +2083,7 @@ def test_logs_fill_gaps_formula_with_group_by(
|
||||
|
||||
def test_logs_fill_zero(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -2092,7 +2102,7 @@ def test_logs_fill_zero(
|
||||
]
|
||||
insert_logs(logs)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -2149,6 +2159,7 @@ def test_logs_fill_zero(
|
||||
|
||||
def test_logs_fill_zero_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -2174,7 +2185,7 @@ def test_logs_fill_zero_with_group_by(
|
||||
]
|
||||
insert_logs(logs)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -2248,6 +2259,7 @@ def test_logs_fill_zero_with_group_by(
|
||||
|
||||
def test_logs_fill_zero_formula(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -2273,7 +2285,7 @@ def test_logs_fill_zero_formula(
|
||||
]
|
||||
insert_logs(logs)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -2355,6 +2367,7 @@ def test_logs_fill_zero_formula(
|
||||
|
||||
def test_logs_fill_zero_formula_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -2380,7 +2393,7 @@ def test_logs_fill_zero_formula_with_group_by(
|
||||
]
|
||||
insert_logs(logs)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
@@ -6,12 +6,13 @@ from typing import Callable, List
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logs import Logs
|
||||
|
||||
|
||||
def test_logs_json_body_simple_searches(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -85,7 +86,7 @@ def test_logs_json_body_simple_searches(
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)
|
||||
|
||||
# Test 1: Search by body.message = "User logged in successfully"
|
||||
response = requests.post(
|
||||
@@ -300,6 +301,7 @@ def test_logs_json_body_simple_searches(
|
||||
|
||||
def test_logs_json_body_nested_keys(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -392,7 +394,7 @@ def test_logs_json_body_nested_keys(
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)
|
||||
|
||||
# Test 1: Search by body.user.name = "john_doe"
|
||||
response = requests.post(
|
||||
@@ -569,6 +571,7 @@ def test_logs_json_body_nested_keys(
|
||||
|
||||
def test_logs_json_body_array_membership(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -645,7 +648,7 @@ def test_logs_json_body_array_membership(
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)
|
||||
|
||||
# Test 1: Search by has(body.tags[*], "production")
|
||||
response = requests.post(
|
||||
@@ -776,6 +779,7 @@ def test_logs_json_body_array_membership(
|
||||
|
||||
def test_logs_json_body_listing(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
@@ -873,7 +877,7 @@ def test_logs_json_body_listing(
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(email=USER_ADMIN_EMAIL, password=USER_ADMIN_PASSWORD)
|
||||
|
||||
# Test 1: List all logs with JSON bodies
|
||||
response = requests.post(
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Callable, Dict, List
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import (
|
||||
assert_minutely_bucket_values,
|
||||
@@ -16,6 +16,7 @@ from fixtures.querier import (
|
||||
|
||||
def test_metrics_fill_gaps(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
@@ -45,7 +46,7 @@ def test_metrics_fill_gaps(
|
||||
]
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -109,6 +110,7 @@ def test_metrics_fill_gaps(
|
||||
|
||||
def test_metrics_fill_gaps_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
@@ -137,7 +139,7 @@ def test_metrics_fill_gaps_with_group_by(
|
||||
]
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -217,6 +219,7 @@ def test_metrics_fill_gaps_with_group_by(
|
||||
|
||||
def test_metrics_fill_gaps_formula(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
@@ -246,7 +249,7 @@ def test_metrics_fill_gaps_formula(
|
||||
]
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -339,6 +342,7 @@ def test_metrics_fill_gaps_formula(
|
||||
|
||||
def test_metrics_fill_gaps_formula_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
@@ -381,7 +385,7 @@ def test_metrics_fill_gaps_formula_with_group_by(
|
||||
]
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -494,6 +498,7 @@ def test_metrics_fill_gaps_formula_with_group_by(
|
||||
|
||||
def test_metrics_fill_zero(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
@@ -515,7 +520,7 @@ def test_metrics_fill_zero(
|
||||
]
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -578,6 +583,7 @@ def test_metrics_fill_zero(
|
||||
|
||||
def test_metrics_fill_zero_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
@@ -605,7 +611,7 @@ def test_metrics_fill_zero_with_group_by(
|
||||
]
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -685,6 +691,7 @@ def test_metrics_fill_zero_with_group_by(
|
||||
|
||||
def test_metrics_fill_zero_formula(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
@@ -713,7 +720,7 @@ def test_metrics_fill_zero_formula(
|
||||
]
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -806,6 +813,7 @@ def test_metrics_fill_zero_formula(
|
||||
|
||||
def test_metrics_fill_zero_formula_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
@@ -848,7 +856,7 @@ def test_metrics_fill_zero_formula_with_group_by(
|
||||
]
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import (
|
||||
assert_minutely_bucket_values,
|
||||
find_named_result,
|
||||
@@ -23,6 +23,7 @@ from src.querier.util import (
|
||||
|
||||
def test_traces_list(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
@@ -149,7 +150,7 @@ def test_traces_list(
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Query all traces for the past 5 minutes
|
||||
response = requests.post(
|
||||
@@ -661,6 +662,7 @@ def test_traces_list(
|
||||
)
|
||||
def test_traces_list_with_corrupt_data(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
payload: Dict[str, Any],
|
||||
@@ -678,7 +680,7 @@ def test_traces_list_with_corrupt_data(
|
||||
# 4 Traces with corrupt metadata inserted
|
||||
# traces[i] occured before traces[j] where i < j
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
@@ -764,6 +766,7 @@ def test_traces_list_with_corrupt_data(
|
||||
)
|
||||
def test_traces_aggergate_order_by_count(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
order_by: Dict[str, str],
|
||||
@@ -890,7 +893,7 @@ def test_traces_aggergate_order_by_count(
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
query = {
|
||||
"type": "builder_query",
|
||||
@@ -934,6 +937,7 @@ def test_traces_aggergate_order_by_count(
|
||||
|
||||
def test_traces_aggregate_with_mixed_field_selectors(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
@@ -1057,7 +1061,7 @@ def test_traces_aggregate_with_mixed_field_selectors(
|
||||
]
|
||||
)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
query = {
|
||||
"type": "builder_query",
|
||||
@@ -1110,6 +1114,7 @@ def test_traces_aggregate_with_mixed_field_selectors(
|
||||
|
||||
def test_traces_fill_gaps(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
@@ -1136,7 +1141,7 @@ def test_traces_fill_gaps(
|
||||
]
|
||||
insert_traces(traces)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -1193,6 +1198,7 @@ def test_traces_fill_gaps(
|
||||
|
||||
def test_traces_fill_gaps_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
@@ -1231,7 +1237,7 @@ def test_traces_fill_gaps_with_group_by(
|
||||
]
|
||||
insert_traces(traces)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -1305,6 +1311,7 @@ def test_traces_fill_gaps_with_group_by(
|
||||
|
||||
def test_traces_fill_gaps_formula(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
@@ -1343,7 +1350,7 @@ def test_traces_fill_gaps_formula(
|
||||
]
|
||||
insert_traces(traces)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -1424,6 +1431,7 @@ def test_traces_fill_gaps_formula(
|
||||
|
||||
def test_traces_fill_gaps_formula_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
@@ -1462,7 +1470,7 @@ def test_traces_fill_gaps_formula_with_group_by(
|
||||
]
|
||||
insert_traces(traces)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -1564,6 +1572,7 @@ def test_traces_fill_gaps_formula_with_group_by(
|
||||
|
||||
def test_traces_fill_zero(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
@@ -1589,7 +1598,7 @@ def test_traces_fill_zero(
|
||||
]
|
||||
insert_traces(traces)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -1646,6 +1655,7 @@ def test_traces_fill_zero(
|
||||
|
||||
def test_traces_fill_zero_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
@@ -1684,7 +1694,7 @@ def test_traces_fill_zero_with_group_by(
|
||||
]
|
||||
insert_traces(traces)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -1759,6 +1769,7 @@ def test_traces_fill_zero_with_group_by(
|
||||
|
||||
def test_traces_fill_zero_formula(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
@@ -1797,7 +1808,7 @@ def test_traces_fill_zero_formula(
|
||||
]
|
||||
insert_traces(traces)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
@@ -1878,6 +1889,7 @@ def test_traces_fill_zero_formula(
|
||||
|
||||
def test_traces_fill_zero_formula_with_group_by(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
@@ -1916,7 +1928,7 @@ def test_traces_fill_zero_formula_with_group_by(
|
||||
]
|
||||
insert_traces(traces)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
start_ms = int((now - timedelta(minutes=5)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
|
||||
@@ -8,7 +8,7 @@ from http import HTTPStatus
|
||||
from typing import Any, Callable, List
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import (
|
||||
build_builder_query,
|
||||
@@ -23,6 +23,7 @@ CUMULATIVE_COUNTERS_FILE = os.path.join(TESTDATA_DIR, "cumulative_counters_1h.js
|
||||
|
||||
def test_rate_with_steady_values_and_reset(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
@@ -38,7 +39,7 @@ def test_rate_with_steady_values_and_reset(
|
||||
)
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = build_builder_query(
|
||||
"A",
|
||||
metric_name,
|
||||
@@ -72,6 +73,7 @@ def test_rate_with_steady_values_and_reset(
|
||||
|
||||
def test_rate_group_by_endpoint(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
@@ -87,7 +89,7 @@ def test_rate_group_by_endpoint(
|
||||
)
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = build_builder_query(
|
||||
"A",
|
||||
metric_name,
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Callable, Dict, List, Optional, Tuple
|
||||
import requests
|
||||
|
||||
from fixtures import querier, types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
|
||||
@@ -210,13 +210,14 @@ def build_metrics_query(
|
||||
|
||||
def test_logs_scalar_group_by_single_agg_no_order(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -237,13 +238,14 @@ def test_logs_scalar_group_by_single_agg_no_order(
|
||||
|
||||
def test_logs_scalar_group_by_single_agg_order_by_agg_asc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -264,13 +266,14 @@ def test_logs_scalar_group_by_single_agg_order_by_agg_asc(
|
||||
|
||||
def test_logs_scalar_group_by_single_agg_order_by_agg_desc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -291,13 +294,14 @@ def test_logs_scalar_group_by_single_agg_order_by_agg_desc(
|
||||
|
||||
def test_logs_scalar_group_by_single_agg_order_by_grouping_key_asc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -322,13 +326,14 @@ def test_logs_scalar_group_by_single_agg_order_by_grouping_key_asc(
|
||||
|
||||
def test_logs_scalar_group_by_single_agg_order_by_grouping_key_desc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -353,13 +358,14 @@ def test_logs_scalar_group_by_single_agg_order_by_grouping_key_desc(
|
||||
|
||||
def test_logs_scalar_group_by_multiple_aggs_order_by_first_agg_asc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -384,13 +390,14 @@ def test_logs_scalar_group_by_multiple_aggs_order_by_first_agg_asc(
|
||||
|
||||
def test_logs_scalar_group_by_multiple_aggs_order_by_second_agg_desc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -416,13 +423,14 @@ def test_logs_scalar_group_by_multiple_aggs_order_by_second_agg_desc(
|
||||
|
||||
def test_logs_scalar_group_by_single_agg_order_by_agg_asc_limit_2(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -447,13 +455,14 @@ def test_logs_scalar_group_by_single_agg_order_by_agg_asc_limit_2(
|
||||
|
||||
def test_logs_scalar_group_by_single_agg_order_by_agg_desc_limit_3(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -478,13 +487,14 @@ def test_logs_scalar_group_by_single_agg_order_by_agg_desc_limit_3(
|
||||
|
||||
def test_logs_scalar_group_by_order_by_grouping_key_asc_limit_2(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_logs: Callable[[List[Logs]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_logs(generate_logs_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -509,13 +519,14 @@ def test_logs_scalar_group_by_order_by_grouping_key_asc_limit_2(
|
||||
|
||||
def test_traces_scalar_group_by_single_agg_no_order(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -536,13 +547,14 @@ def test_traces_scalar_group_by_single_agg_no_order(
|
||||
|
||||
def test_traces_scalar_group_by_single_agg_order_by_agg_asc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -563,13 +575,14 @@ def test_traces_scalar_group_by_single_agg_order_by_agg_asc(
|
||||
|
||||
def test_traces_scalar_group_by_single_agg_order_by_agg_desc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -590,13 +603,14 @@ def test_traces_scalar_group_by_single_agg_order_by_agg_desc(
|
||||
|
||||
def test_traces_scalar_group_by_single_agg_order_by_grouping_key_asc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -621,13 +635,14 @@ def test_traces_scalar_group_by_single_agg_order_by_grouping_key_asc(
|
||||
|
||||
def test_traces_scalar_group_by_single_agg_order_by_grouping_key_desc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -652,13 +667,14 @@ def test_traces_scalar_group_by_single_agg_order_by_grouping_key_desc(
|
||||
|
||||
def test_traces_scalar_group_by_multiple_aggs_order_by_first_agg_asc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -683,13 +699,14 @@ def test_traces_scalar_group_by_multiple_aggs_order_by_first_agg_asc(
|
||||
|
||||
def test_traces_scalar_group_by_single_agg_order_by_agg_asc_limit_2(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -714,13 +731,14 @@ def test_traces_scalar_group_by_single_agg_order_by_agg_asc_limit_2(
|
||||
|
||||
def test_traces_scalar_group_by_single_agg_order_by_agg_desc_limit_3(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -745,13 +763,14 @@ def test_traces_scalar_group_by_single_agg_order_by_agg_desc_limit_3(
|
||||
|
||||
def test_traces_scalar_group_by_order_by_grouping_key_asc_limit_2(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_traces: Callable[[List[Traces]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_traces(generate_traces_with_counts(now, log_or_trace_service_counts))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -776,13 +795,14 @@ def test_traces_scalar_group_by_order_by_grouping_key_asc_limit_2(
|
||||
|
||||
def test_metrics_scalar_group_by_single_agg_no_order(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_metrics(generate_metrics_with_values(now, metric_values_for_test))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -808,13 +828,14 @@ def test_metrics_scalar_group_by_single_agg_no_order(
|
||||
|
||||
def test_metrics_scalar_group_by_single_agg_order_by_agg_asc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_metrics(generate_metrics_with_values(now, metric_values_for_test))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -845,13 +866,14 @@ def test_metrics_scalar_group_by_single_agg_order_by_agg_asc(
|
||||
|
||||
def test_metrics_scalar_group_by_single_agg_order_by_grouping_key_asc(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_metrics(generate_metrics_with_values(now, metric_values_for_test))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -882,13 +904,14 @@ def test_metrics_scalar_group_by_single_agg_order_by_grouping_key_asc(
|
||||
|
||||
def test_metrics_scalar_group_by_single_agg_order_by_agg_asc_limit_2(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_metrics(generate_metrics_with_values(now, metric_values_for_test))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -915,13 +938,14 @@ def test_metrics_scalar_group_by_single_agg_order_by_agg_asc_limit_2(
|
||||
|
||||
def test_metrics_scalar_group_by_single_agg_order_by_agg_desc_limit_3(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_metrics(generate_metrics_with_values(now, metric_values_for_test))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
@@ -948,13 +972,14 @@ def test_metrics_scalar_group_by_single_agg_order_by_agg_desc_limit_3(
|
||||
|
||||
def test_metrics_scalar_group_by_order_by_grouping_key_asc_limit_2(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
) -> None:
|
||||
now = datetime.now(tz=timezone.utc).replace(second=0, microsecond=0)
|
||||
insert_metrics(generate_metrics_with_values(now, metric_values_for_test))
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_scalar_query_request(
|
||||
signoz,
|
||||
token,
|
||||
|
||||
@@ -10,7 +10,7 @@ from typing import Callable, List
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import (
|
||||
build_builder_query,
|
||||
@@ -38,6 +38,7 @@ MULTI_TEMPORALITY_FILE_24h = get_testdata_file_path(
|
||||
)
|
||||
def test_with_steady_values_and_reset(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
time_aggregation: str,
|
||||
@@ -57,7 +58,7 @@ def test_with_steady_values_and_reset(
|
||||
)
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = build_builder_query(
|
||||
"A",
|
||||
metric_name,
|
||||
@@ -98,6 +99,7 @@ def test_with_steady_values_and_reset(
|
||||
)
|
||||
def test_group_by_endpoint(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
time_aggregation: str,
|
||||
@@ -120,7 +122,7 @@ def test_group_by_endpoint(
|
||||
)
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = build_builder_query(
|
||||
"A",
|
||||
metric_name,
|
||||
@@ -280,6 +282,7 @@ def test_group_by_endpoint(
|
||||
)
|
||||
def test_for_service_with_switch(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
time_aggregation: str,
|
||||
@@ -299,7 +302,7 @@ def test_for_service_with_switch(
|
||||
)
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = build_builder_query(
|
||||
"A",
|
||||
metric_name,
|
||||
@@ -336,6 +339,7 @@ def test_for_service_with_switch(
|
||||
)
|
||||
def test_for_week_long_time_range(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
time_aggregation: str,
|
||||
@@ -353,7 +357,7 @@ def test_for_week_long_time_range(
|
||||
)
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = build_builder_query(
|
||||
"A",
|
||||
metric_name,
|
||||
@@ -379,6 +383,7 @@ def test_for_week_long_time_range(
|
||||
)
|
||||
def test_for_month_long_time_range(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[List[Metrics]], None],
|
||||
time_aggregation: str,
|
||||
@@ -396,7 +401,7 @@ def test_for_month_long_time_range(
|
||||
)
|
||||
insert_metrics(metrics)
|
||||
|
||||
token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
query = build_builder_query(
|
||||
"A",
|
||||
metric_name,
|
||||
|
||||
@@ -5,17 +5,18 @@ import pytest
|
||||
import requests
|
||||
from sqlalchemy import sql
|
||||
|
||||
from fixtures.signoz import ROOT_USER_EMAIL, ROOT_USER_PASSWORD
|
||||
from fixtures.types import SigNoz
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.types import Operation, SigNoz
|
||||
|
||||
ANONYMOUS_USER_ID = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
|
||||
def test_managed_roles_create_on_register(
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# get the list of all roles.
|
||||
response = requests.get(
|
||||
@@ -44,9 +45,10 @@ def test_managed_roles_create_on_register(
|
||||
def test_root_user_signoz_admin_assignment(
|
||||
request: pytest.FixtureRequest,
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
# Get the user from the /user/me endpoint and extract the id
|
||||
user_response = requests.get(
|
||||
@@ -104,9 +106,10 @@ def test_root_user_signoz_admin_assignment(
|
||||
def test_anonymous_user_signoz_anonymous_assignment(
|
||||
request: pytest.FixtureRequest,
|
||||
signoz: SigNoz,
|
||||
create_user_admin: Operation, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
):
|
||||
admin_token = get_token(ROOT_USER_EMAIL, ROOT_USER_PASSWORD)
|
||||
admin_token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_configs["8080"].get("/api/v1/roles"),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user