Compare commits

..

4 Commits

Author SHA1 Message Date
Naman Verma
c808b4d759 fix: consume order by and limit for metrics in clickhouse query 2026-03-05 09:29:23 +05:30
Vinicius Lourenço
850dc10983 fix(antd): wrong usage of imports (#10491)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-03-05 00:49:26 +05:30
SagarRajput-7
e8e85a3790 feat: added roles crud and details page (#10384)
* feat: added roles crud and details page

* feat: added details page content

* feat: added empty state in members tab

* feat: added permission detail side panel

* feat: made managed roles read only

* feat: added authzresources call in the app context

* feat: refactored files, made constants and utils

* feat: updated test mocks and util

* feat: added redirect to details page upon creation and other refactoring

* feat: used enum and refactoring

* feat: removed sidepanel toggle and changed the buildpayload logic

* feat: moved authz resource to roles page from app context

* feat: semantic token usage

* feat: cleanup, comment address and refactor

* feat: cleanup, comment address and refactor

* feat: temporarily hide the crud and role details page

* feat: added test cases for the role details flow and utilities

* feat: added test config in jest and refactored code

* feat: changed the page availability condition based on the tenant license type

* feat: resolved merge conflict changes in settings util
2026-03-04 19:03:40 +05:30
SagarRajput-7
20d9d5ae38 feat: redesign the custom domain and moved it to general settings (#10432)
* feat: redesign the custom domain and moved it to general settings

* feat: cleanup and refactor

* feat: updated and added test cases

* feat: updated home page data source info section

* feat: cleanup and refactor

* feat: comment addressed

* feat: comment addressed

* feat: scss refactoring with semantic tokens

* feat: update icons to use from signozhq

* feat: added test config in jest and refactored code

---------

Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
2026-03-04 18:41:24 +05:30
235 changed files with 4924 additions and 3480 deletions

View File

@@ -42,6 +42,10 @@ const config: Config.InitialOptions = {
'<rootDir>/node_modules/@signozhq/command/dist/command.js',
'^@signozhq/radio-group':
'<rootDir>/node_modules/@signozhq/radio-group/dist/radio-group.js',
'^@signozhq/toggle-group$':
'<rootDir>/node_modules/@signozhq/toggle-group/dist/toggle-group.js',
'^@signozhq/dialog$':
'<rootDir>/node_modules/@signozhq/dialog/dist/dialog.js',
},
extensionsToTreatAsEsm: ['.ts'],
testMatch: ['<rootDir>/src/**/*?(*.)(test).(ts|js)?(x)'],

View File

@@ -54,8 +54,7 @@
"@signozhq/combobox": "0.0.2",
"@signozhq/command": "0.0.0",
"@signozhq/design-tokens": "2.1.1",
"@signozhq/dialog": "0.0.2",
"@signozhq/drawer": "0.0.4",
"@signozhq/dialog": "^0.0.2",
"@signozhq/icons": "0.1.0",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
@@ -64,6 +63,7 @@
"@signozhq/sonner": "0.1.0",
"@signozhq/switch": "0.0.2",
"@signozhq/table": "0.3.7",
"@signozhq/toggle-group": "^0.0.1",
"@signozhq/tooltip": "0.0.2",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",

View File

@@ -14,5 +14,5 @@
"archives": "Archives",
"logs_to_metrics": "Logs To Metrics",
"roles": "Roles",
"members": "Members"
"role_details": "Role Details"
}

View File

@@ -14,5 +14,5 @@
"archives": "Archives",
"logs_to_metrics": "Logs To Metrics",
"roles": "Roles",
"members": "Members"
"role_details": "Role Details"
}

View File

@@ -74,6 +74,5 @@
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer Views",
"METER": "SigNoz | Meter",
"ROLES_SETTINGS": "SigNoz | Roles",
"MEMBERS_SETTINGS": "SigNoz | Members"
"ROLES_SETTINGS": "SigNoz | Roles"
}

View File

@@ -165,11 +165,6 @@ export const MySettings = Loadable(
() => import(/* webpackChunkName: "All MySettings" */ 'pages/Settings'),
);
export const CustomDomainSettings = Loadable(
() =>
import(/* webpackChunkName: "Custom Domain Settings" */ 'pages/Settings'),
);
export const Logs = Loadable(
() => import(/* webpackChunkName: "Logs" */ 'pages/LogsModulePage'),
);

View File

@@ -19,7 +19,6 @@ import '@signozhq/combobox';
import '@signozhq/command';
import '@signozhq/design-tokens';
import '@signozhq/dialog';
import '@signozhq/drawer';
import '@signozhq/icons';
import '@signozhq/input';
import '@signozhq/popover';
@@ -28,4 +27,5 @@ import '@signozhq/resizable';
import '@signozhq/sonner';
import '@signozhq/switch';
import '@signozhq/table';
import '@signozhq/toggle-group';
import '@signozhq/tooltip';

View File

@@ -16,7 +16,7 @@ import {
Tooltip,
Typography,
} from 'antd';
import { FilterDropdownProps } from 'antd/lib/table/interface';
import type { FilterDropdownProps } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
import {
getQueueOverview,

View File

@@ -1,7 +1,7 @@
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';

View File

@@ -1,324 +0,0 @@
.edit-member-drawer {
&__layout {
display: flex;
flex-direction: column;
height: calc(100vh - 48px);
}
&__body {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px 16px;
}
&__field {
display: flex;
flex-direction: column;
gap: 8px;
}
&__label {
font-size: 14px;
font-weight: 400;
color: var(--vanilla-400, #c0c1c3);
line-height: 20px;
letter-spacing: -0.07px;
cursor: default;
}
&__input {
height: 32px;
width: 100%;
background: var(--bg-ink-300, #16181d) !important;
border-color: var(--slate-400, #1d212d) !important;
color: var(--vanilla-100, #fff) !important;
border-radius: 2px;
&::placeholder {
color: var(--vanilla-400, #c0c1c3);
}
}
&__input-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
padding: 0 8px;
border-radius: 2px;
background: var(--bg-ink-300, #16181d);
border: 1px solid var(--slate-400, #1d212d);
&--disabled {
cursor: not-allowed;
opacity: 0.8;
}
}
&__email-text {
font-size: 14px;
font-weight: 400;
color: var(--vanilla-400, #c0c1c3);
line-height: 18px;
letter-spacing: -0.07px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
&__lock-icon {
color: var(--vanilla-400, #c0c1c3);
flex-shrink: 0;
margin-left: 6px;
opacity: 0.6;
}
&__role-select {
width: 100%;
height: 32px;
.ant-select-selector {
height: 32px !important;
background: var(--bg-ink-300, #16181d) !important;
border-color: var(--slate-400, #1d212d) !important;
border-radius: 2px !important;
padding: 0 8px !important;
display: flex;
align-items: center;
}
.ant-select-selection-item {
font-size: 14px;
color: var(--vanilla-100, #fff);
line-height: 32px !important;
letter-spacing: -0.07px;
}
.ant-select-arrow {
color: var(--vanilla-400, #c0c1c3);
}
&:not(.ant-select-disabled):hover .ant-select-selector {
border-color: var(--vanilla-400, #c0c1c3) !important;
}
}
&__meta {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 4px;
}
&__meta-item {
display: flex;
flex-direction: column;
gap: 4px;
}
&__meta-label {
font-family: Inter, sans-serif;
font-size: 12px;
font-weight: 500;
color: var(--vanilla-400, #c0c1c3);
line-height: 20px;
letter-spacing: 0.48px;
text-transform: uppercase;
}
&__footer {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 56px;
padding: 0 16px;
border-top: 1px solid var(--slate-400, #1d212d);
flex-shrink: 0;
background: var(--bg-ink-400, #121317);
}
&__footer-left {
display: flex;
align-items: center;
gap: 16px;
}
&__footer-right {
display: flex;
align-items: center;
gap: 12px;
}
&__footer-divider {
width: 1px;
height: 21px;
background: var(--slate-400, #1d212d);
flex-shrink: 0;
}
&__footer-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
font-family: Inter, sans-serif;
font-size: 11px;
font-weight: 400;
line-height: 1;
letter-spacing: 0;
transition: opacity 0.15s ease;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&:not(:disabled):hover {
opacity: 0.8;
}
&--danger {
color: var(--bg-cherry-500, #e5484d);
}
&--warning {
color: var(--bg-amber-500, #ffcd56);
}
}
&__delete-body {
font-size: 13px;
font-weight: 400;
color: var(--vanilla-400, #b0b2b4);
line-height: 20px;
letter-spacing: -0.065px;
margin: 0;
strong {
font-weight: 500;
color: var(--vanilla-100, #fff);
}
}
&__delete-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 24px;
}
}
.reset-link-dialog {
[data-slot='dialog-description'] {
width: 510px;
}
&__content {
display: flex;
flex-direction: column;
gap: 16px;
}
&__description {
font-size: 13px;
font-weight: 400;
color: var(--vanilla-400, #b0b2b4);
line-height: 20px;
letter-spacing: -0.065px;
margin: 0;
white-space: normal;
word-break: break-word;
}
&__link-row {
display: flex;
align-items: center;
height: 32px;
overflow: hidden;
background: var(--bg-ink-300, #16181d);
border: 1px solid var(--slate-400, #1d212d);
border-radius: 2px;
}
&__link-text-wrap {
flex: 1;
min-width: 0;
overflow: hidden;
}
&__link-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 8px;
font-size: 14px;
font-weight: 400;
color: var(--vanilla-400, #c0c1c3);
line-height: 18px;
letter-spacing: -0.07px;
}
&__copy-btn {
flex-shrink: 0;
height: 32px !important;
border-radius: 0 2px 2px 0 !important;
border-top: none !important;
border-right: none !important;
border-bottom: none !important;
border-left: 1px solid var(--slate-400, #1d212d) !important;
min-width: 64px;
}
}
.lightMode {
.edit-member-drawer {
&__label,
&__email-text,
&__meta-label {
color: #5a5f6e;
}
&__input {
background: #f5f5f7 !important;
border-color: #d9d9d9 !important;
color: #1d1f29 !important;
}
&__input-wrapper {
background: #f5f5f7;
border-color: #d9d9d9;
}
&__role-select {
.ant-select-selector {
background: #f5f5f7 !important;
border-color: #d9d9d9 !important;
}
.ant-select-selection-item {
color: #1d1f29;
}
}
&__footer-divider {
background: #e8e8e8;
}
&__delete-body {
color: #5a5f6e;
strong {
color: #1d1f29;
}
}
}
}

View File

@@ -1,471 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { Badge } from '@signozhq/badge';
import { Button } from '@signozhq/button';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { DrawerWrapper } from '@signozhq/drawer';
import {
Check,
ChevronDown,
Copy,
Link,
LockKeyhole,
RefreshCw,
Trash2,
X,
} from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { toast } from '@signozhq/sonner';
import { Select } from 'antd';
import getResetPasswordToken from 'api/v1/factor_password/getResetPasswordToken';
import sendInvite from 'api/v1/invite/create';
import cancelInvite from 'api/v1/invite/id/delete';
import deleteUser from 'api/v1/user/id/delete';
import update from 'api/v1/user/id/update';
import { MemberRow } from 'components/MembersTable/MembersTable';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import ROUTES from 'constants/routes';
import { useTimezone } from 'providers/Timezone';
import { ROLES } from 'types/roles';
import './EditMemberDrawer.styles.scss';
export interface EditMemberDrawerProps {
member: MemberRow | null;
open: boolean;
onClose: () => void;
onSuccess: () => void;
}
const INVITE_PREFIX = 'invite-';
function formatRoleLabel(role: string): string {
return role.charAt(0).toUpperCase() + role.slice(1).toLowerCase();
}
// eslint-disable-next-line sonarjs/cognitive-complexity
function EditMemberDrawer({
member,
open,
onClose,
onSuccess,
}: EditMemberDrawerProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const [displayName, setDisplayName] = useState('');
const [selectedRole, setSelectedRole] = useState<ROLES>('VIEWER');
const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isGeneratingLink, setIsGeneratingLink] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [resetLink, setResetLink] = useState<string | null>(null);
const [showResetLinkDialog, setShowResetLinkDialog] = useState(false);
const [hasCopiedResetLink, setHasCopiedResetLink] = useState(false);
const isInvited = member?.status === 'Invited';
// Invited member IDs are prefixed with 'invite-'; strip it to get the real invite ID
const inviteId =
isInvited && member ? member.id.slice(INVITE_PREFIX.length) : null;
useEffect(() => {
if (member) {
setDisplayName(member.name);
setSelectedRole(member.role);
}
}, [member]);
const isDirty =
member !== null &&
(displayName !== member.name || selectedRole !== member.role);
const formatTimestamp = useCallback(
(ts: string | null | undefined): string => {
if (!ts) {
return '—';
}
const d = new Date(ts);
if (Number.isNaN(d.getTime())) {
return '—';
}
return formatTimezoneAdjustedTimestamp(ts, DATE_TIME_FORMATS.DASH_DATETIME);
},
[formatTimezoneAdjustedTimestamp],
);
const handleSave = useCallback(async (): Promise<void> => {
if (!member || !isDirty) {
return;
}
setIsSaving(true);
try {
if (isInvited && inviteId) {
await cancelInvite({ id: inviteId });
await sendInvite({
email: member.email,
name: displayName,
role: selectedRole,
frontendBaseUrl: window.location.origin,
});
toast.success('Invite updated successfully', { richColors: true });
} else {
await update({
userId: member.id,
displayName,
role: selectedRole,
});
toast.success('Member details updated successfully', { richColors: true });
}
onSuccess();
onClose();
} catch {
toast.error(
isInvited ? 'Failed to update invite' : 'Failed to update member details',
{ richColors: true },
);
} finally {
setIsSaving(false);
}
}, [
member,
isDirty,
isInvited,
inviteId,
displayName,
selectedRole,
onSuccess,
onClose,
]);
const handleDelete = useCallback(async (): Promise<void> => {
if (!member) {
return;
}
setIsDeleting(true);
try {
if (isInvited && inviteId) {
await cancelInvite({ id: inviteId });
toast.success('Invitation cancelled successfully', { richColors: true });
} else {
await deleteUser({ userId: member.id });
toast.success('Member deleted successfully', { richColors: true });
}
setShowDeleteConfirm(false);
onSuccess();
onClose();
} catch {
toast.error(
isInvited ? 'Failed to cancel invitation' : 'Failed to delete member',
{ richColors: true },
);
} finally {
setIsDeleting(false);
}
}, [member, isInvited, inviteId, onSuccess, onClose]);
const handleGenerateResetLink = useCallback(async (): Promise<void> => {
if (!member) {
return;
}
setIsGeneratingLink(true);
try {
const response = await getResetPasswordToken({ userId: member.id });
if (response?.data?.token) {
const link = `${window.location.origin}/password-reset?token=${response.data.token}`;
setResetLink(link);
setHasCopiedResetLink(false);
setShowResetLinkDialog(true);
onClose();
}
} catch {
toast.error('Failed to generate password reset link', {
richColors: true,
position: 'top-right',
});
} finally {
setIsGeneratingLink(false);
}
}, [member, onClose]);
const handleCopyResetLink = useCallback(async (): Promise<void> => {
if (!resetLink) {
return;
}
try {
await navigator.clipboard.writeText(resetLink);
setHasCopiedResetLink(true);
setTimeout(() => setHasCopiedResetLink(false), 2000);
toast.success('Reset link copied to clipboard', { richColors: true });
} catch {
toast.error('Failed to copy link', {
richColors: true,
});
}
}, [resetLink]);
const handleCopyInviteLink = useCallback(async (): Promise<void> => {
if (!member?.token) {
toast.error('Invite link is not available', {
richColors: true,
position: 'top-right',
});
return;
}
const inviteLink = `${window.location.origin}${ROUTES.SIGN_UP}?token=${member.token}`;
try {
await navigator.clipboard.writeText(inviteLink);
toast.success('Invite link copied to clipboard', {
richColors: true,
position: 'top-right',
});
} catch {
toast.error('Failed to copy invite link', {
richColors: true,
position: 'top-right',
});
}
}, [member]);
const handleClose = useCallback((): void => {
setShowDeleteConfirm(false);
onClose();
}, [onClose]);
const joinedOnLabel = isInvited ? 'Invited On' : 'Joined On';
const drawerContent = (
<div className="edit-member-drawer__layout">
<div className="edit-member-drawer__body">
<div className="edit-member-drawer__field">
<label className="edit-member-drawer__label" htmlFor="member-name">
Name
</label>
<Input
id="member-name"
value={displayName}
onChange={(e): void => setDisplayName(e.target.value)}
className="edit-member-drawer__input"
placeholder="Enter name"
/>
</div>
<div className="edit-member-drawer__field">
<label className="edit-member-drawer__label" htmlFor="member-email">
Email Address
</label>
<div className="edit-member-drawer__input-wrapper edit-member-drawer__input-wrapper--disabled">
<span className="edit-member-drawer__email-text">
{member?.email || '—'}
</span>
<LockKeyhole size={16} className="edit-member-drawer__lock-icon" />
</div>
</div>
<div className="edit-member-drawer__field">
<label className="edit-member-drawer__label" htmlFor="member-role">
Roles
</label>
<Select
id="member-role"
value={selectedRole}
onChange={(role): void => setSelectedRole(role as ROLES)}
className="edit-member-drawer__role-select"
suffixIcon={<ChevronDown size={14} />}
getPopupContainer={(triggerNode): HTMLElement =>
(triggerNode?.closest('.edit-member-drawer') as HTMLElement) ||
document.body
}
>
<Select.Option value="ADMIN">{formatRoleLabel('ADMIN')}</Select.Option>
<Select.Option value="EDITOR">{formatRoleLabel('EDITOR')}</Select.Option>
<Select.Option value="VIEWER">{formatRoleLabel('VIEWER')}</Select.Option>
</Select>
</div>
<div className="edit-member-drawer__meta">
<div className="edit-member-drawer__meta-item">
<span className="edit-member-drawer__meta-label">Status</span>
{member?.status === 'Active' ? (
<Badge color="forest" variant="outline">
ACTIVE
</Badge>
) : (
<Badge color="amber" variant="outline">
INVITED
</Badge>
)}
</div>
<div className="edit-member-drawer__meta-item">
<span className="edit-member-drawer__meta-label">{joinedOnLabel}</span>
<Badge color="vanilla">{formatTimestamp(member?.joinedOn)}</Badge>
</div>
{!isInvited && (
<div className="edit-member-drawer__meta-item">
<span className="edit-member-drawer__meta-label">Last Modified</span>
<Badge color="vanilla">{formatTimestamp(member?.updatedAt)}</Badge>
</div>
)}
</div>
</div>
<div className="edit-member-drawer__footer">
<div className="edit-member-drawer__footer-left">
<button
type="button"
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--danger"
onClick={(): void => setShowDeleteConfirm(true)}
>
<Trash2 size={12} />
{isInvited ? 'Cancel Invite' : 'Delete Member'}
</button>
<div className="edit-member-drawer__footer-divider" />
{isInvited ? (
<button
type="button"
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
onClick={handleCopyInviteLink}
disabled={!member?.token}
>
<Link size={12} />
Copy Invite Link
</button>
) : (
<button
type="button"
className="edit-member-drawer__footer-btn edit-member-drawer__footer-btn--warning"
onClick={handleGenerateResetLink}
disabled={isGeneratingLink}
>
<RefreshCw size={12} />
{isGeneratingLink ? 'Generating...' : 'Generate Password Reset Link'}
</button>
)}
</div>
<div className="edit-member-drawer__footer-right">
<Button variant="solid" color="secondary" size="sm" onClick={handleClose}>
<X size={14} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="sm"
disabled={!isDirty || isSaving}
onClick={handleSave}
>
{isSaving ? 'Saving...' : 'Save Member Details'}
</Button>
</div>
</div>
</div>
);
const deleteDialogTitle = isInvited ? 'Cancel Invitation' : 'Delete Member';
const deleteDialogBody = isInvited
? `Are you sure you want to cancel the invitation for ${member?.email}? They will no longer be able to join the workspace using this invite.`
: `Are you sure you want to delete ${
member?.name || member?.email
}? This will permanently remove their access to the workspace.`;
const deleteConfirmLabel = isInvited ? 'Cancel Invite' : 'Delete Member';
return (
<>
<DrawerWrapper
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
handleClose();
}
}}
direction="right"
type="panel"
showCloseButton
showOverlay={false}
allowOutsideClick
header={{ title: 'Member Details' }}
content={drawerContent}
className="edit-member-drawer"
/>
<DialogWrapper
open={showResetLinkDialog}
onOpenChange={(isOpen): void => {
if (!isOpen) {
setShowResetLinkDialog(false);
}
}}
title="Password Reset Link"
showCloseButton
width="base"
className="reset-link-dialog"
>
<div className="reset-link-dialog__content">
<p className="reset-link-dialog__description">
This creates a one-time link the team member can use to set a new password
for their SigNoz account.
</p>
<div className="reset-link-dialog__link-row">
<div className="reset-link-dialog__link-text-wrap">
<span className="reset-link-dialog__link-text">{resetLink}</span>
</div>
<Button
variant="outlined"
color="secondary"
size="sm"
onClick={handleCopyResetLink}
prefixIcon={
hasCopiedResetLink ? <Check size={12} /> : <Copy size={12} />
}
className="reset-link-dialog__copy-btn"
>
{hasCopiedResetLink ? 'Copied!' : 'Copy'}
</Button>
</div>
</div>
</DialogWrapper>
<DialogWrapper
open={showDeleteConfirm}
onOpenChange={(isOpen): void => {
if (!isOpen) {
setShowDeleteConfirm(false);
}
}}
title={deleteDialogTitle}
width="narrow"
className="alert-dialog"
showCloseButton={false}
disableOutsideClick={false}
>
<p className="edit-member-drawer__delete-body">{deleteDialogBody}</p>
<DialogFooter className="edit-member-drawer__delete-footer">
<Button
variant="solid"
color="secondary"
size="sm"
onClick={(): void => setShowDeleteConfirm(false)}
>
<X size={12} />
Cancel
</Button>
<Button
variant="solid"
color="destructive"
size="sm"
disabled={isDeleting}
onClick={handleDelete}
>
<Trash2 size={12} />
{isDeleting ? 'Processing...' : deleteConfirmLabel}
</Button>
</DialogFooter>
</DialogWrapper>
</>
);
}
export default EditMemberDrawer;

View File

@@ -13,7 +13,8 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('antd/es/form/Form', () => ({
jest.mock('antd', () => ({
...jest.requireActual('antd'),
useForm: jest.fn().mockReturnValue({
onFinish: jest.fn(),
}),

View File

@@ -1,6 +1,6 @@
import { UseMutateAsyncFunction } from 'react-query';
import { FormInstance } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import type { NotificationInstance } from 'antd/es/notification/interface';
import { AxiosResponse } from 'axios';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { OptionsQuery } from 'container/OptionsMenu/types';

View File

@@ -1,4 +1,4 @@
import { NotificationInstance } from 'antd/es/notification/interface';
import type { NotificationInstance } from 'antd/es/notification/interface';
import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';

View File

@@ -1,5 +1,4 @@
import { Tag, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { TableColumnsType as ColumnsType, Tag, Typography } from 'antd';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import {

View File

@@ -12,7 +12,7 @@ import {
Tag,
Typography,
} from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import type { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';

View File

@@ -1,420 +0,0 @@
.invite-members-modal {
display: flex !important;
flex-direction: column !important;
max-width: 700px !important;
padding: 0 !important;
gap: 0 !important;
background: #121317 !important;
border: 1px solid #161922 !important;
border-radius: 4px !important;
box-shadow: 0 4px 9px 0 rgba(0, 0, 0, 0.04) !important;
[data-slot='dialog-header'] {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #161922;
margin: 0;
flex-shrink: 0;
background: transparent;
}
[data-slot='dialog-title'] {
font-family: Inter, sans-serif;
font-size: 13px;
font-weight: 400;
line-height: 1;
letter-spacing: -0.065px;
color: #ffffff;
margin: 0;
}
// Close button rendered by DialogWrapper
[data-slot='dialog-close'] {
color: #c0c1c3;
opacity: 0.7;
transition: opacity 0.15s;
background: transparent;
border: none;
&:hover {
opacity: 1;
}
}
}
// ─── Content area ─────────────────────────────────────────────────────────────
.invite-members-modal__content {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
flex: 1;
}
// ─── Table Structure ──────────────────────────────────────────────────────────
.invite-members-modal__table {
width: 100%;
min-height: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.invite-members-modal__table-header {
display: flex;
gap: 16px;
align-items: center;
flex-shrink: 0;
height: auto;
width: 100%;
justify-content: flex-end;
.email-header {
flex: 0 0 220px;
width: 220px;
}
.role-header {
flex: 1 0 0;
min-width: 0;
}
.action-header {
flex: 0 0 32px;
width: 32px;
}
.table-header-cell {
color: var(--l1-foreground);
font-family: Inter, sans-serif;
font-size: 13px;
font-style: normal;
font-weight: 600;
line-height: 100%;
letter-spacing: -0.065px;
}
}
.invite-members-modal__container {
display: flex;
flex-direction: column;
gap: 16px !important;
width: 100%;
flex: 0 1 auto;
min-height: 0;
overflow-x: hidden;
}
// ─── Rows & Cells ─────────────────────────────────────────────────────────────
.team-member-row {
display: flex;
width: 100%;
gap: 16px;
> div.email-cell {
flex: 0 0 220px;
width: 220px;
}
> div.role-cell {
flex: 1 0 0;
min-width: 0;
}
> div.action-cell {
flex: 0 0 32px;
width: 32px;
}
}
.team-member-cell {
display: flex;
flex-direction: column;
gap: 8px;
&.action-cell {
display: flex;
align-items: flex-end;
justify-content: center;
height: 32px;
}
}
// ─── Form Elements ────────────────────────────────────────────────────────────
.team-member-email-input {
width: 100%;
height: 32px !important;
border-radius: 2px;
background: var(--l3-background);
border: 1px solid var(--l3-border);
color: var(--l1-foreground);
font-family: Inter, sans-serif;
font-size: 13px;
font-weight: 400;
line-height: 1;
letter-spacing: -0.065px;
padding: 6px 8px;
box-sizing: border-box;
&::placeholder {
color: var(--l3-foreground);
}
&:hover {
border-color: var(--l3-border);
}
&:focus {
border-color: var(--bg-robin-500);
box-shadow: none;
}
}
.team-member-role-select {
width: 100%;
.ant-select-selector {
height: 32px !important;
border-radius: 2px !important;
background: var(--l3-background) !important;
border: 1px solid var(--l3-border) !important;
color: var(--l1-foreground) !important;
font-family: Inter, sans-serif !important;
font-size: 13px !important;
font-weight: 400 !important;
line-height: 1 !important;
letter-spacing: -0.065px !important;
padding: 0 8px !important;
box-sizing: border-box !important;
.ant-select-selection-placeholder {
color: var(--l3-foreground) !important;
}
.ant-select-selection-item {
color: var(--l1-foreground) !important;
line-height: 30px !important;
}
}
.ant-select-arrow {
color: var(--l3-foreground) !important;
}
&.ant-select-focused .ant-select-selector {
border-color: var(--bg-robin-500) !important;
}
&:hover .ant-select-selector {
border-color: var(--bg-robin-500) !important;
}
}
// ─── Actions ──────────────────────────────────────────────────────────────────
.remove-team-member-button {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 32px !important;
height: 32px !important;
min-width: 32px !important;
border: none !important;
border-radius: 2px !important;
background: transparent !important;
color: #e5484d !important;
opacity: 0.6 !important;
cursor: pointer;
padding: 0 !important;
transition: background-color 0.2s, opacity 0.2s;
box-shadow: none !important;
svg {
color: #e5484d !important;
width: 14px !important;
height: 14px !important;
}
&:hover {
background: rgba(229, 72, 77, 0.1) !important;
opacity: 0.9 !important;
color: #e5484d !important;
svg {
color: #e5484d !important;
}
}
&:active {
opacity: 0.7 !important;
background: rgba(229, 72, 77, 0.15) !important;
}
&:focus-visible {
outline: 2px solid var(--bg-robin-500);
outline-offset: 2px;
}
}
// ─── Validation ───────────────────────────────────────────────────────────────
.email-error-message {
font-size: 12px;
font-weight: 400;
line-height: 16px;
margin-top: 4px;
width: 100%;
display: block;
color: var(--bg-cherry-500);
}
.invite-team-members-error-callout {
background: rgba(229, 72, 77, 0.1);
border: 1px solid rgba(229, 72, 77, 0.2);
border-radius: 4px;
animation: horizontal-shaking 300ms ease-out;
}
@keyframes horizontal-shaking {
0% {
transform: translateX(0);
}
25% {
transform: translateX(5px);
}
50% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
100% {
transform: translateX(0);
}
}
// ─── Footer ───────────────────────────────────────────────────────────────────
.invite-members-modal__footer {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 16px !important;
height: 64px !important;
min-height: 64px !important;
border-top: 1px solid #161922 !important;
gap: 0 !important;
flex-shrink: 0;
}
.invite-members-modal__footer-right {
display: flex;
align-items: center;
gap: 12px;
}
.add-another-member-button {
&:hover {
border-color: var(--bg-robin-500) !important;
border-style: dashed !important;
color: var(--l1-foreground);
svg,
[class*='icon'] {
color: var(--l1-foreground) !important;
}
}
}
// ─── Light mode overrides ─────────────────────────────────────────────────────
.lightMode {
.invite-members-modal {
background: #ffffff !important;
border-color: #e9e9e9 !important;
[data-slot='dialog-header'] {
border-bottom-color: #e9e9e9;
}
[data-slot='dialog-title'] {
color: #121317;
}
}
.invite-members-modal__table-header {
.table-header-cell {
color: var(--l3-foreground);
}
}
.team-member-email-input {
background: var(--bg-vanilla-200) !important;
border-color: var(--bg-vanilla-300) !important;
color: var(--text-ink-500) !important;
&::placeholder {
color: var(--text-neutral-light-200) !important;
}
&:hover {
border-color: var(--bg-vanilla-300) !important;
}
&:focus {
border-color: var(--bg-robin-500) !important;
}
}
.team-member-role-select {
.ant-select-selector {
background: var(--l3-background) !important;
border: 1px solid var(--l3-border) !important;
color: var(--l1-foreground) !important;
.ant-select-selection-placeholder {
color: var(--l3-foreground) !important;
}
.ant-select-selection-item {
color: var(--l1-foreground) !important;
}
}
.ant-select-arrow {
color: var(--l3-foreground) !important;
}
}
.remove-team-member-button {
color: var(--bg-cherry-500) !important;
svg {
color: var(--bg-cherry-500) !important;
}
&:hover {
background: rgba(245, 101, 101, 0.1) !important;
}
}
.add-another-member-button {
border-color: var(--text-vanilla-300) !important;
color: var(--l3-foreground);
svg {
color: var(--l3-foreground) !important;
}
&:hover {
border-color: var(--bg-robin-500) !important;
color: var(--l1-foreground);
background: rgba(78, 116, 248, 0.1) !important;
svg {
color: var(--l1-foreground) !important;
}
}
}
.invite-members-modal__footer {
border-top-color: #e9e9e9 !important;
}
}

View File

@@ -1,335 +0,0 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Button } from '@signozhq/button';
import { Callout } from '@signozhq/callout';
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
import { Input } from '@signozhq/input';
import { toast } from '@signozhq/sonner';
import { Select, Typography } from 'antd';
import sendInvite from 'api/v1/invite/create';
import { cloneDeep, debounce } from 'lodash-es';
import { ChevronDown, CircleAlert, Plus, Trash2, X } from 'lucide-react';
import { ROLES } from 'types/roles';
import { v4 as uuid } from 'uuid';
import './InviteMembersModal.styles.scss';
interface InviteRow {
id: string;
email: string;
role: ROLES | '';
}
export interface InviteMembersModalProps {
open: boolean;
onClose: () => void;
onSuccess?: () => void;
}
const EMPTY_ROW = (): InviteRow => ({ id: uuid(), email: '', role: '' });
const isRowTouched = (row: InviteRow): boolean =>
row.email.trim() !== '' || Boolean(row.role && row.role.trim() !== '');
function InviteMembersModal({
open,
onClose,
onSuccess,
}: InviteMembersModalProps): JSX.Element {
const [rows, setRows] = useState<InviteRow[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [emailValidity, setEmailValidity] = useState<Record<string, boolean>>(
{},
);
const [hasInvalidEmails, setHasInvalidEmails] = useState<boolean>(false);
const [hasInvalidRoles, setHasInvalidRoles] = useState<boolean>(false);
const resetAndClose = useCallback((): void => {
setRows([EMPTY_ROW(), EMPTY_ROW(), EMPTY_ROW()]);
setEmailValidity({});
setHasInvalidEmails(false);
setHasInvalidRoles(false);
onClose();
}, [onClose]);
useEffect(() => {
if (open) {
setRows([EMPTY_ROW(), EMPTY_ROW(), EMPTY_ROW()]);
}
}, [open]);
const getValidationErrorMessage = (): string => {
if (hasInvalidEmails && hasInvalidRoles) {
return 'Please enter valid emails and select roles for team members';
}
if (hasInvalidEmails) {
return 'Please enter valid emails for team members';
}
return 'Please select roles for team members';
};
const validateAllUsers = useCallback((): boolean => {
let isValid = true;
let hasEmailErrors = false;
let hasRoleErrors = false;
const updatedEmailValidity: Record<string, boolean> = {};
const touchedRows = rows.filter(isRowTouched);
touchedRows.forEach((row) => {
const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(row.email);
const roleValid = Boolean(row.role && row.role.trim() !== '');
if (!emailValid || !row.email) {
isValid = false;
hasEmailErrors = true;
}
if (!roleValid) {
isValid = false;
hasRoleErrors = true;
}
if (row.id) {
updatedEmailValidity[row.id] = emailValid;
}
});
setEmailValidity(updatedEmailValidity);
setHasInvalidEmails(hasEmailErrors);
setHasInvalidRoles(hasRoleErrors);
return isValid;
}, [rows]);
const debouncedValidateEmail = useMemo(
() =>
debounce((email: string, rowId: string) => {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
setEmailValidity((prev) => ({ ...prev, [rowId]: isValid }));
}, 500),
[],
);
const updateEmail = (id: string, email: string): void => {
const updatedRows = cloneDeep(rows);
const rowToUpdate = updatedRows.find((r) => r.id === id);
if (rowToUpdate) {
rowToUpdate.email = email;
setRows(updatedRows);
if (hasInvalidEmails) {
setHasInvalidEmails(false);
}
if (emailValidity[id] === false) {
setEmailValidity((prev) => ({ ...prev, [id]: true }));
}
debouncedValidateEmail(email, id);
}
};
const updateRole = (id: string, role: ROLES): void => {
const updatedRows = cloneDeep(rows);
const rowToUpdate = updatedRows.find((r) => r.id === id);
if (rowToUpdate) {
rowToUpdate.role = role;
setRows(updatedRows);
if (hasInvalidRoles) {
setHasInvalidRoles(false);
}
}
};
const addRow = (): void => {
setRows((prev) => [...prev, EMPTY_ROW()]);
};
const removeRow = (id: string): void => {
setRows((prev) => prev.filter((r) => r.id !== id));
};
const handleSubmit = useCallback(async (): Promise<void> => {
if (!validateAllUsers()) {
return;
}
const touchedRows = rows.filter(isRowTouched);
if (touchedRows.length === 0) {
return;
}
setIsSubmitting(true);
let hasError = false;
await Promise.all(
touchedRows.map(
async (row): Promise<void> => {
try {
await sendInvite({
email: row.email.trim(),
name: '',
role: row.role as ROLES,
frontendBaseUrl: window.location.origin,
});
} catch (err: unknown) {
hasError = true;
const apiErr = err as {
response?: { status: number };
getErrorMessage?: () => string;
};
if (apiErr?.response?.status === 409) {
toast.error(`${row.email} is already a member`, { richColors: true });
} else {
const errorMessage = apiErr?.getErrorMessage?.() ?? 'An error occurred';
toast.error(`Failed to invite ${row.email}: ${errorMessage}`, {
richColors: true,
});
}
}
},
),
);
setIsSubmitting(false);
if (!hasError) {
toast.success('Invites sent successfully', { richColors: true });
resetAndClose();
onSuccess?.();
} else {
onSuccess?.();
}
}, [rows, onSuccess, resetAndClose, validateAllUsers]);
const touchedRows = rows.filter(isRowTouched);
const isSubmitDisabled = isSubmitting || touchedRows.length === 0;
return (
<DialogWrapper
title="Invite Team Members"
open={open}
onOpenChange={(isOpen): void => {
if (!isOpen) {
resetAndClose();
}
}}
showCloseButton
width="wide"
className="invite-members-modal"
disableOutsideClick
>
<div className="invite-members-modal__content">
<div className="invite-members-modal__table">
<div className="invite-members-modal__table-header">
<div className="table-header-cell email-header">Email address</div>
<div className="table-header-cell role-header">Roles</div>
<div className="table-header-cell action-header" />
</div>
<div className="invite-members-modal__container">
{rows.map(
(row): JSX.Element => (
<div key={row.id} className="team-member-row">
<div className="team-member-cell email-cell">
<Input
type="email"
placeholder="john@signoz.io"
value={row.email}
onChange={(e): void => updateEmail(row.id, e.target.value)}
className="team-member-email-input"
/>
{emailValidity[row.id] === false && row.email.trim() !== '' && (
<Typography.Text className="email-error-message">
Invalid email address
</Typography.Text>
)}
</div>
<div className="team-member-cell role-cell">
<Select
value={row.role || undefined}
onChange={(role): void => updateRole(row.id, role as ROLES)}
className="team-member-role-select"
placeholder="Select roles"
suffixIcon={<ChevronDown size={14} />}
getPopupContainer={(triggerNode): HTMLElement =>
(triggerNode?.closest('.invite-members-modal') as HTMLElement) ||
document.body
}
>
<Select.Option value="VIEWER">Viewer</Select.Option>
<Select.Option value="EDITOR">Editor</Select.Option>
<Select.Option value="ADMIN">Admin</Select.Option>
</Select>
</div>
<div className="team-member-cell action-cell">
{rows.length > 1 && (
<Button
variant="ghost"
color="secondary"
className="remove-team-member-button"
onClick={(): void => removeRow(row.id)}
aria-label="Remove row"
>
<Trash2 size={12} />
</Button>
)}
</div>
</div>
),
)}
</div>
</div>
{(hasInvalidEmails || hasInvalidRoles) && (
<Callout
type="error"
size="small"
showIcon
icon={<CircleAlert size={12} />}
className="invite-team-members-error-callout"
description={getValidationErrorMessage()}
/>
)}
</div>
{/* Footer */}
<DialogFooter className="invite-members-modal__footer">
<Button
variant="dashed"
color="secondary"
size="sm"
className="add-another-member-button"
prefixIcon={<Plus size={12} />}
onClick={addRow}
>
Add another
</Button>
<div className="invite-members-modal__footer-right">
<Button
type="button"
variant="solid"
color="secondary"
size="sm"
onClick={resetAndClose}
>
<X size={12} />
Cancel
</Button>
<Button
variant="solid"
color="primary"
size="sm"
onClick={handleSubmit}
disabled={isSubmitDisabled}
>
{isSubmitting ? 'Inviting...' : 'Invite Team Members'}
</Button>
</div>
</DialogFooter>
</DialogWrapper>
);
}
export default InviteMembersModal;

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import type { RadioChangeEvent } from 'antd/lib';
import cx from 'classnames';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';

View File

@@ -1,4 +1,7 @@
import { ColumnsType, ColumnType } from 'antd/es/table';
import {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';

View File

@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { TableColumnsType as ColumnsType, Typography } from 'antd';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Input, InputNumber, Popover, Tooltip, Typography } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';

View File

@@ -1,250 +0,0 @@
// ─── Table wrapper ────────────────────────────────────────────────────────────
.members-table-wrapper {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
border-radius: 4px;
}
// ─── Ant Design Table overrides ───────────────────────────────────────────────
.members-table {
// Flatten Ant Design chrome
.ant-table {
background: transparent;
font-size: 14px;
}
.ant-table-container {
border-radius: 0 !important;
border: none !important;
}
// ── Header ──────────────────────────────────────────────────────────────
.ant-table-thead {
> tr > th,
> tr > td {
background: var(--bg-ink-500, #0b0c0e);
font-family: Inter, sans-serif;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.44px;
text-transform: uppercase;
color: var(--vanilla-400, #c0c1c3);
line-height: 18px;
padding: 8px 16px;
border-bottom: none !important;
border-top: none !important;
// Remove the column divider pseudo-element
&::before {
display: none !important;
}
// Sorter icon color
.ant-table-column-sorter {
color: var(--vanilla-400, #c0c1c3);
opacity: 0.6;
}
.ant-table-column-sorter-up.active,
.ant-table-column-sorter-down.active {
color: var(--vanilla-100, #ffffff);
opacity: 1;
}
}
}
// ── Body rows ────────────────────────────────────────────────────────────
.ant-table-tbody {
> tr > td {
border-bottom: none !important;
padding: 8px 16px;
background: transparent;
transition: none;
}
// Even-indexed rows (0, 2, 4 …) get a subtle tint — matches Figma
> tr.members-table-row--tinted > td {
background: rgba(171, 189, 255, 0.02);
}
// Hover — slightly brighter tint
> tr:hover > td {
background: rgba(171, 189, 255, 0.04) !important;
}
}
// Remove outer table border
.ant-table-wrapper,
.ant-table-container,
.ant-spin-nested-loading,
.ant-spin-container {
border: none !important;
box-shadow: none !important;
}
// ── Status cell — right-align badge and add 4px right padding ─────────
.member-status-cell {
padding-right: 4px !important;
}
}
// ─── Name / Email cell ────────────────────────────────────────────────────────
.member-name-email-cell {
display: flex;
align-items: center;
gap: 4px;
height: 22px;
overflow: hidden;
.member-name {
font-size: 14px;
font-weight: 500;
color: var(--vanilla-400, #c0c1c3);
line-height: 20px;
letter-spacing: -0.07px;
white-space: nowrap;
flex-shrink: 0;
}
.member-email {
font-size: 14px;
font-weight: 400;
color: var(--slate-50, #62687c);
line-height: 20px;
letter-spacing: -0.07px;
flex: 1 0 0;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
// ─── Joined On ────────────────────────────────────────────────────────────────
.member-joined-date {
font-size: 14px;
font-weight: 400;
color: var(--vanilla-400, #c0c1c3);
line-height: 18px;
letter-spacing: -0.07px;
white-space: nowrap;
}
.member-joined-dash {
font-size: 14px;
color: var(--slate-50, #62687c);
line-height: 18px;
letter-spacing: -0.07px;
}
// ─── Empty state ─────────────────────────────────────────────────────────────
.members-empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 16px;
gap: 8px;
&__emoji {
font-size: 24px;
line-height: 1;
}
&__text {
font-size: 14px;
font-weight: 400;
color: var(--vanilla-400, #c0c1c3);
margin: 0;
line-height: 20px;
strong {
font-weight: 500;
color: var(--vanilla-100, #ffffff);
}
}
}
// ─── Pagination ───────────────────────────────────────────────────────────────
.members-table-pagination {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 8px 16px;
.ant-pagination-total-text {
margin-right: auto;
}
.members-pagination-range {
font-family: Inter, sans-serif;
font-size: 12px;
color: var(--vanilla-400, #c0c1c3);
}
.members-pagination-total {
font-family: Inter, sans-serif;
font-size: 12px;
color: var(--vanilla-400, #c0c1c3);
opacity: 0.5;
}
}
// ─── Light mode overrides ─────────────────────────────────────────────────────
.lightMode {
.members-table {
.ant-table-thead {
> tr > th,
> tr > td {
background: #f5f5f7;
color: #5a5f6e;
}
}
.ant-table-tbody {
> tr.members-table-row--tinted > td {
background: rgba(0, 0, 0, 0.015);
}
> tr:hover > td {
background: rgba(0, 0, 0, 0.03) !important;
}
}
}
.member-name-email-cell {
.member-name {
color: #1d1f29;
}
.member-email {
color: #5a5f6e;
}
}
.member-joined-dash {
color: #5a5f6e;
}
.member-joined-date {
color: #1d1f29;
}
.members-empty-state {
&__text {
color: #5a5f6e;
strong {
color: #1d1f29;
}
}
}
.members-pagination-range,
.members-pagination-total {
color: #5a5f6e;
}
}

View File

@@ -1,232 +0,0 @@
import type React from 'react';
import { Badge } from '@signozhq/badge';
import { Pagination, Table, Tooltip } from 'antd';
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { useTimezone } from 'providers/Timezone';
import { ROLES } from 'types/roles';
import './MembersTable.styles.scss';
export interface MemberRow {
id: string;
name: string;
email: string;
role: ROLES;
status: 'Active' | 'Invited';
joinedOn: string | null;
updatedAt?: string | null;
token?: string | null;
}
interface MembersTableProps {
data: MemberRow[];
loading: boolean;
total: number;
currentPage: number;
pageSize: number;
searchQuery: string;
onPageChange: (page: number) => void;
onRowClick?: (member: MemberRow) => void;
onSortChange?: (
sorter: SorterResult<MemberRow> | SorterResult<MemberRow>[],
) => void;
}
function formatRoleLabel(role: string): string {
return role.charAt(0).toUpperCase() + role.slice(1).toLowerCase();
}
function NameEmailCell({
name,
email,
}: {
name: string;
email: string;
}): JSX.Element {
return (
<div className="member-name-email-cell">
{name && (
<span className="member-name" title={name}>
{name}
</span>
)}
<Tooltip title={email} overlayClassName="member-tooltip">
<span className="member-email">{email}</span>
</Tooltip>
</div>
);
}
function StatusBadge({ status }: { status: MemberRow['status'] }): JSX.Element {
if (status === 'Active') {
return (
<Badge color="forest" variant="outline">
ACTIVE
</Badge>
);
}
return (
<Badge color="amber" variant="outline">
INVITED
</Badge>
);
}
function MembersEmptyState({
searchQuery,
}: {
searchQuery: string;
}): JSX.Element {
return (
<div className="members-empty-state">
<span className="members-empty-state__emoji">🧐</span>
{searchQuery ? (
<p className="members-empty-state__text">
No results for <strong>{searchQuery}</strong>
</p>
) : (
<p className="members-empty-state__text">No members found</p>
)}
</div>
);
}
function MembersTable({
data,
loading,
total,
currentPage,
pageSize,
searchQuery,
onPageChange,
onRowClick,
onSortChange,
}: MembersTableProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const formatJoinedOn = (date: string | null): string => {
if (!date) {
return '—';
}
const d = new Date(date);
if (Number.isNaN(d.getTime())) {
return '—';
}
return formatTimezoneAdjustedTimestamp(date, DATE_TIME_FORMATS.DASH_DATETIME);
};
const columns: ColumnsType<MemberRow> = [
{
title: 'Name / Email',
dataIndex: 'name',
key: 'name',
render: (_, record): JSX.Element => (
<NameEmailCell name={record.name} email={record.email} />
),
},
{
title: 'Roles',
dataIndex: 'role',
key: 'role',
width: 180,
render: (role: ROLES): JSX.Element => (
<Badge color="vanilla">{formatRoleLabel(role)}</Badge>
),
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 100,
align: 'right' as const,
className: 'member-status-cell',
sorter: (a, b): number => a.status.localeCompare(b.status),
render: (status: MemberRow['status']): JSX.Element => (
<StatusBadge status={status} />
),
},
{
title: 'Joined On',
dataIndex: 'joinedOn',
key: 'joinedOn',
width: 250,
align: 'right' as const,
sorter: (a, b): number => {
if (!a.joinedOn && !b.joinedOn) {
return 0;
}
if (!a.joinedOn) {
return 1;
}
if (!b.joinedOn) {
return -1;
}
return new Date(a.joinedOn).getTime() - new Date(b.joinedOn).getTime();
},
render: (joinedOn: string | null): JSX.Element => {
const formatted = formatJoinedOn(joinedOn);
const isDash = formatted === '—';
return (
<span className={isDash ? 'member-joined-dash' : 'member-joined-date'}>
{formatted}
</span>
);
},
},
];
const showPaginationTotal = (_total: number, range: number[]): JSX.Element => (
<>
<span className="members-pagination-range">
{range[0]} &#8212; {range[1]}
</span>
<span className="members-pagination-total"> of {_total}</span>
</>
);
return (
<div className="members-table-wrapper">
<Table<MemberRow>
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={false}
rowClassName={(_, index): string =>
index % 2 === 0 ? 'members-table-row--tinted' : ''
}
onRow={(record): React.HTMLAttributes<HTMLElement> => ({
onClick: (): void => onRowClick?.(record),
style: onRowClick ? { cursor: 'pointer' } : undefined,
})}
onChange={(_, __, sorter): void => {
if (onSortChange) {
onSortChange(
sorter as SorterResult<MemberRow> | SorterResult<MemberRow>[],
);
}
}}
showSorterTooltip={false}
locale={{
emptyText: <MembersEmptyState searchQuery={searchQuery} />,
}}
className="members-table"
/>
{total > pageSize && (
<Pagination
current={currentPage}
pageSize={pageSize}
total={total}
showTotal={showPaginationTotal}
showSizeChanger={false}
onChange={onPageChange}
className="members-table-pagination"
/>
)}
</div>
);
}
export default MembersTable;

View File

@@ -1,6 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import { Info } from 'lucide-react';
import './MQCommon.styles.scss';

View File

@@ -1,7 +1,10 @@
import { memo, useEffect, useState } from 'react';
import type {
TableColumnGroupType as ColumnGroupType,
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { ColumnGroupType, ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useSafeNavigate } from 'hooks/useSafeNavigate';

View File

@@ -9,7 +9,7 @@ import {
import ReactDragListView from 'react-drag-listview';
import { ResizeCallbackData } from 'react-resizable';
import { Table } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import type { ColumnsType } from 'antd/lib/table';
import cx from 'classnames';
import { dragColumnParams } from 'hooks/useDragColumns/configs';
import { getColumnWidth, RowData } from 'lib/query/createTableColumnsFromQuery';

View File

@@ -1,7 +1,7 @@
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { PaginationProps } from 'antd/lib';
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import type { ColumnsType } from 'antd/es/table';
import type { PaginationProps } from 'antd/lib';
import type { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { LaunchChatSupportProps } from 'components/LaunchChatSupport/LaunchChatSupport';
import { TableDataSource } from './contants';

View File

@@ -1,5 +1,4 @@
import { Radio } from 'antd';
import { RadioChangeEvent } from 'antd/es/radio';
import { Radio, RadioChangeEvent } from 'antd';
import './SignozRadioGroup.styles.scss';

View File

@@ -13,8 +13,8 @@ import {
TabsProps,
Typography,
} from 'antd';
import { TextProps } from 'antd/lib/typography/Text';
import { TitleProps } from 'antd/lib/typography/Title';
import type { TextProps } from 'antd/lib/typography/Text';
import type { TitleProps } from 'antd/lib/typography/Title';
import styled, { FlattenSimpleInterpolation } from 'styled-components';
import { IStyledClass } from './types';

View File

@@ -1,5 +1,7 @@
import { ColumnType } from 'antd/es/table';
import { ColumnsType } from 'antd/lib/table';
import type {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
export const generatorResizeTableColumns = <T>({
baseColumnOptions,

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Radio } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import type { RadioChangeEvent } from 'antd/lib';
import { History, Table } from 'lucide-react';
import { ALERT_TABS } from '../constants';

View File

@@ -1,7 +1,6 @@
import React, { Dispatch, SetStateAction, useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { Tag } from 'antd/lib';
import { Button, Tag } from 'antd';
import Input from 'components/Input';
import { Check, X } from 'lucide-react';

View File

@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { WarningFilled } from '@ant-design/icons';
import { Select, Tooltip } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import classNames from 'classnames';
import { UniversalYAxisUnitMappings } from './constants';

View File

@@ -38,7 +38,6 @@ const ROUTES = {
SETTINGS: '/settings',
MY_SETTINGS: '/settings/my-settings',
ORG_SETTINGS: '/settings/org-settings',
CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings',
API_KEYS: '/settings/api-keys',
INGESTION_SETTINGS: '/settings/ingestion-settings',
SOMETHING_WENT_WRONG: '/something-went-wrong',
@@ -56,7 +55,7 @@ const ROUTES = {
TRACE_EXPLORER: '/trace-explorer',
BILLING: '/settings/billing',
ROLES_SETTINGS: '/settings/roles',
MEMBERS_SETTINGS: '/settings/members',
ROLE_DETAILS: '/settings/roles/:roleId',
SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces/saved-views',

View File

@@ -8,6 +8,7 @@ import {
Button,
Col,
Collapse,
CollapseProps,
Flex,
Form,
Input,
@@ -20,8 +21,7 @@ import {
Tooltip,
Typography,
} from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import { CollapseProps } from 'antd/lib';
import type { NotificationInstance } from 'antd/es/notification/interface';
import createAPIKeyApi from 'api/v1/pats/create';
import deleteAPIKeyApi from 'api/v1/pats/delete';
import updateAPIKeyApi from 'api/v1/pats/update';

View File

@@ -1,7 +1,6 @@
import { HTMLAttributes } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Progress, Table } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { Progress, Table, TableColumnsType as ColumnsType } from 'antd';
import logEvent from 'api/common/logEvent';
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
import AlertLabels from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';

View File

@@ -1,8 +1,7 @@
import { useMemo } from 'react';
import { EllipsisOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { Button, TableColumnsType as ColumnsType } from 'antd';
import ClientSideQBSearch, {
AttributeKey,
} from 'components/ClientSideQBSearch/ClientSideQBSearch';

View File

@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { Button } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import type { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { Button } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import type { NotificationInstance } from 'antd/es/notification/interface';
import deleteChannel from 'api/channels/delete';
import APIError from 'types/api/error';

View File

@@ -14,9 +14,9 @@ import {
Tooltip,
Typography,
} from 'antd';
import { ColumnType, TablePaginationConfig } from 'antd/es/table';
import { FilterValue, SorterResult } from 'antd/es/table/interface';
import { ColumnsType } from 'antd/lib/table';
import type { ColumnType, TablePaginationConfig } from 'antd/es/table';
import type { FilterValue, SorterResult } from 'antd/es/table/interface';
import type { ColumnsType } from 'antd/lib/table';
import { FilterConfirmProps } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
import getAll from 'api/errors/getAll';

View File

@@ -1,4 +1,4 @@
import { FilterDropdownProps } from 'antd/es/table/interface';
import type { FilterDropdownProps } from 'antd/es/table/interface';
export interface FilterDropdownExtendsProps {
placeholder: string;

View File

@@ -1,4 +1,4 @@
import { FilterValue, SortOrder } from 'antd/lib/table/interface';
import type { FilterValue, SortOrder } from 'antd/lib/table/interface';
import Timestamp from 'timestamp-nano';
import { Order, OrderBy } from 'types/api/errors/getAll';

View File

@@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import type { RadioChangeEvent } from 'antd/lib';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,

View File

@@ -1,7 +1,6 @@
import { useMemo, useState } from 'react';
import { UseQueryResult } from 'react-query';
import { Table, TablePaginationConfig, Typography } from 'antd';
import Skeleton from 'antd/lib/skeleton';
import { Skeleton, Table, TablePaginationConfig, Typography } from 'antd';
import { QueryParams } from 'constants/query';
import {
dependentServicesColumns,

View File

@@ -4,7 +4,7 @@ import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { LoadingOutlined } from '@ant-design/icons';
import { Spin, Table } from 'antd';
import { ColumnType } from 'antd/lib/table';
import type { ColumnType } from 'antd/lib/table';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';

View File

@@ -69,9 +69,6 @@ jest.mock('antd', () => {
))}
</div>
)),
Skeleton: jest
.fn()
.mockImplementation(() => <div data-testid="skeleton-mock" />),
Typography: {
Text: jest
.fn()

View File

@@ -1,7 +1,6 @@
import { ReactNode } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Progress, Tag, Tooltip } from 'antd';
import { ColumnType } from 'antd/es/table';
import { Progress, TableColumnType as ColumnType, Tag, Tooltip } from 'antd';
import { convertFiltersToExpressionWithExistingQuery } from 'components/QueryBuilderV2/utils';
import {
FiltersType,

View File

@@ -12,10 +12,10 @@ import {
Row,
Skeleton,
Table,
TableColumnsType as ColumnsType,
Tag,
Typography,
} from 'antd';
import { ColumnsType } from 'antd/es/table';
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
import logEvent from 'api/common/logEvent';
import updateCreditCardApi from 'api/v1/checkout/create';

View File

@@ -1,5 +1,5 @@
import { Link } from 'react-router-dom';
import Breadcrumb from 'antd/es/breadcrumb';
import { Breadcrumb } from 'antd';
import ROUTES from 'constants/routes';
import { Blocks, LifeBuoy } from 'lucide-react';

View File

@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import { Color } from '@signozhq/design-tokens';
import { Button, Select, Skeleton } from 'antd';
import { SelectProps } from 'antd/lib';
import type { SelectProps } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { useAwsAccounts } from 'hooks/integration/aws/useAwsAccounts';
import useUrlQuery from 'hooks/useUrlQuery';

View File

@@ -1,5 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
import * as appHooks from 'providers/App/App';

View File

@@ -1,4 +1,4 @@
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import { Channels } from 'types/api/channels/getAll';
import {

View File

@@ -1,5 +1,5 @@
import { Button, Flex, Switch, Typography } from 'antd';
import { BaseOptionType, DefaultOptionType, SelectProps } from 'antd/es/select';
import { Button, Flex, SelectProps, Switch, Typography } from 'antd';
import type { BaseOptionType, DefaultOptionType } from 'antd/es/select';
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';

View File

@@ -1,6 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { Button, DatePicker, Select, Typography } from 'antd';
import TextArea from 'antd/lib/input/TextArea';
import { Button, DatePicker, Input, Select, Typography } from 'antd';
import classNames from 'classnames';
import { useCreateAlertState } from 'container/CreateAlertV2/context';
import { AdvancedOptionsState } from 'container/CreateAlertV2/context/types';
@@ -206,7 +205,7 @@ function EvaluationCadenceDetails({
}
/>
</div>
<TextArea
<Input.TextArea
value={evaluationCadence.rrule.rrule}
placeholder="Enter RRule"
onChange={(value): void =>

View File

@@ -1,5 +1,4 @@
import { Tooltip, Typography } from 'antd';
import TextArea from 'antd/lib/input/TextArea';
import { Input, Tooltip, Typography } from 'antd';
import { Info } from 'lucide-react';
import { useCreateAlertState } from '../context';
@@ -76,7 +75,7 @@ function NotificationMessage(): JSX.Element {
</Popover> */}
</div>
</div>
<TextArea
<Input.TextArea
value={notificationSettings.description}
onChange={(e): void =>
setNotificationSettings({

View File

@@ -0,0 +1,202 @@
import { useEffect, useState } from 'react';
import { Button } from '@signozhq/button';
import { Color } from '@signozhq/design-tokens';
import { DialogWrapper } from '@signozhq/dialog';
import { CircleAlert, CircleCheck, LoaderCircle } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
interface CustomDomainEditModalProps {
isOpen: boolean;
onClose: () => void;
customDomainSubdomain?: string;
dnsSuffix: string;
isLoading: boolean;
updateDomainError: AxiosError<RenderErrorResponseDTO> | null;
onClearError: () => void;
onSubmit: (subdomain: string) => void;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function CustomDomainEditModal({
isOpen,
onClose,
customDomainSubdomain,
dnsSuffix,
isLoading,
updateDomainError,
onClearError,
onSubmit,
}: CustomDomainEditModalProps): JSX.Element {
const [value, setValue] = useState(customDomainSubdomain ?? '');
const [validationError, setValidationError] = useState<string | null>(null);
useEffect(() => {
if (isOpen) {
setValue(customDomainSubdomain ?? '');
}
}, [isOpen, customDomainSubdomain]);
const handleClose = (): void => {
setValidationError(null);
onClearError();
onClose();
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setValue(e.target.value);
setValidationError(null);
onClearError();
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
handleSubmit();
}
};
const handleSubmit = (): void => {
if (!value) {
setValidationError('This field is required');
return;
}
if (value.length < 3) {
setValidationError('Minimum 3 characters required');
return;
}
onSubmit(value);
};
const is409 = updateDomainError?.status === 409;
const apiErrorMessage =
(updateDomainError?.response?.data as RenderErrorResponseDTO)?.error
?.message ?? null;
const errorMessage =
validationError ??
(is409
? apiErrorMessage ??
"You've already updated the custom domain once today. Please contact support."
: apiErrorMessage);
const hasError = Boolean(errorMessage);
const statusIcon = ((): JSX.Element => {
if (isLoading) {
return (
<LoaderCircle size={16} className="animate-spin edit-modal-status-icon" />
);
}
if (hasError) {
return <CircleAlert size={16} color={Color.BG_CHERRY_500} />;
}
return <CircleCheck size={16} color={Color.BG_FOREST_500} />;
})();
return (
<DialogWrapper
className="edit-workspace-modal"
title="Edit Workspace Link"
open={isOpen}
onOpenChange={(open: boolean): void => {
if (!open) {
handleClose();
}
}}
width="base"
>
<div className="edit-workspace-modal-content">
<p className="edit-modal-description">
Enter your preferred subdomain to create a unique URL for your team. Need
help?{' '}
<a
href="https://signoz.io/support"
target="_blank"
rel="noreferrer"
className="edit-modal-link"
>
Contact support.
</a>
</p>
<div className="edit-modal-field">
<label
htmlFor="workspace-url-input"
className={`edit-modal-label${
hasError ? ' edit-modal-label--error' : ''
}`}
>
Workspace URL
</label>
<div
className={`edit-modal-input-wrapper${
hasError ? ' edit-modal-input-wrapper--error' : ''
}`}
>
<div className="edit-modal-input-field">
{statusIcon}
<Input
id="workspace-url-input"
aria-describedby="workspace-url-helper"
aria-invalid={hasError}
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
autoFocus
/>
</div>
<div className="edit-modal-input-suffix">{dnsSuffix}</div>
</div>
<span
id="workspace-url-helper"
className={`edit-modal-helper${
hasError ? ' edit-modal-helper--error' : ''
}`}
>
{hasError
? errorMessage
: "To help you easily explore SigNoz, we've selected a tenant sub domain name for you."}
</span>
</div>
<div className="edit-modal-note">
<span className="edit-modal-note-emoji">🚧</span>
<span className="edit-modal-note-text">
Note that your previous URL still remains accessible. Your access
credentials for the new URL remain the same.
</span>
</div>
<div className="edit-modal-footer">
{is409 ? (
<LaunchChatSupport
attributes={{ screen: 'Custom Domain Settings' }}
eventName="Custom Domain Settings: Facing Issues Updating Custom Domain"
message="Hi Team, I need help with updating custom domain"
buttonText="Contact Support"
/>
) : (
<Button
variant="solid"
size="md"
color="primary"
className="edit-modal-apply-btn"
onClick={handleSubmit}
disabled={isLoading}
loading={isLoading}
>
Apply Changes
</Button>
)}
</div>
</div>
</DialogWrapper>
);
}

View File

@@ -1,262 +1,460 @@
.custom-domain-settings-container {
margin-top: 24px;
.beacon {
position: relative;
display: inline-block;
width: 16px;
height: 16px;
flex-shrink: 0;
&::before {
content: '';
position: absolute;
inset: 1px;
border-radius: 50%;
background: rgba(78, 116, 248, 0.2);
}
&::after {
content: '';
position: absolute;
left: 5px;
top: 5px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--primary);
}
}
.custom-domain-card {
width: 100%;
max-width: 768px;
border-radius: 4px;
border: 1px solid var(--l2-border);
background: var(--l2-background);
overflow: hidden;
&--loading {
padding: var(--padding-3);
}
.custom-domain-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: var(--padding-3);
gap: var(--spacing-6);
.custom-domain-edit-button {
border: 1px solid var(--l3-border);
background: var(--l3-background);
&:hover {
background: var(--l3-background-hover);
}
}
}
.custom-domain-card-info {
display: flex;
flex-direction: column;
gap: var(--spacing-6);
}
.custom-domain-card-name-row {
display: flex;
align-items: center;
gap: var(--spacing-5);
}
.custom-domain-card-org-name {
color: var(--l1-foreground);
font-size: var(--paragraph-base-500-font-size);
font-weight: var(--paragraph-base-500-font-weight);
line-height: var(--paragraph-base-500-line-height);
letter-spacing: var(--paragraph-base-500-letter-spacing);
}
.custom-domain-card-meta-row {
display: flex;
align-items: center;
gap: var(--spacing-10);
padding-left: 26px;
}
.custom-domain-card-meta-timezone {
display: inline-flex;
align-items: center;
gap: var(--spacing-3);
color: var(--l1-foreground);
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on;
font-family: Inter;
font-size: var(--paragraph-small-400-font-size);
font-style: normal;
font-weight: var(--paragraph-small-400-font-weight);
line-height: var(--paragraph-small-400-line-height);
letter-spacing: var(--paragraph-small-400-letter-spacing);
text-transform: uppercase;
svg {
flex-shrink: 0;
color: var(--l1-foreground);
}
}
.custom-domain-callout {
margin: 0 var(--margin-3) var(--margin-3);
font-size: var(--paragraph-base-400-font-size);
max-width: 742px;
--callout-background: var(--primary);
--callout-border-color: var(--callout-primary-border);
--callout-icon-color: var(--primary);
--callout-title-color: var(--callout-primary-title);
}
.custom-domain-card-divider {
height: 1px;
background: var(--l2-border);
margin: 0;
}
.custom-domain-card-bottom {
display: flex;
align-items: center;
gap: var(--spacing-5);
padding: var(--padding-3);
}
.custom-domain-card-license {
color: var(--l1-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
.custom-domain-plan-badge {
display: inline-flex;
align-items: center;
padding: 0 2px;
border-radius: 2px;
background: var(--l2-background);
color: var(--l2-foreground);
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
}
}
.workspace-url-trigger {
display: inline-flex;
align-items: center;
gap: var(--spacing-3);
background: none;
border: none;
padding: 0;
cursor: pointer;
color: var(--l1-foreground);
font-size: var(--font-size-xs);
line-height: var(--line-height-18);
letter-spacing: -0.06px;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
svg {
flex-shrink: 0;
color: var(--l2-foreground);
}
}
.workspace-url-dropdown {
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
padding: var(--padding-2) 0;
min-width: 200px;
display: flex;
flex-direction: column;
justify-content: center;
}
.workspace-url-dropdown-header {
color: var(--l2-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
padding: 0 var(--padding-3) var(--padding-2);
}
.workspace-url-dropdown-divider {
height: 1px;
background: var(--l1-border);
margin-bottom: var(--margin-1);
}
.workspace-url-dropdown-item {
display: flex;
align-items: center;
gap: 24px;
justify-content: space-between;
gap: var(--spacing-4);
padding: 5px var(--padding-3);
cursor: pointer;
text-decoration: none;
transition: background 0.15s ease;
&:hover {
background: var(--l1-background-hover);
.workspace-url-dropdown-item-label {
text-decoration: underline;
}
.workspace-url-dropdown-item-external {
opacity: 1;
}
}
&--active {
background: var(--l1-background-hover);
}
}
.workspace-url-dropdown-item-external {
color: var(--l2-foreground);
flex-shrink: 0;
opacity: 0.5;
transition: opacity 0.15s ease;
}
.workspace-url-dropdown-item-label {
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
color: var(--l2-foreground);
.workspace-url-dropdown-item--active & {
color: var(--l1-foreground);
}
}
.workspace-url-dropdown-item-check {
color: var(--l1-foreground);
flex-shrink: 0;
}
.edit-workspace-modal-content {
display: flex;
flex-direction: column;
gap: var(--spacing-12);
}
// Description
.edit-modal-description {
margin: 0;
color: var(--l1-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
.edit-modal-link {
color: var(--primary);
&:hover {
text-decoration: underline;
}
}
// Input field group
.edit-modal-field {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
.edit-modal-label {
color: var(--l2-foreground);
font-size: var(--paragraph-base-500-font-size);
font-weight: var(--paragraph-base-500-font-weight);
line-height: var(--paragraph-base-500-line-height);
&--error {
color: var(--destructive);
}
}
.edit-modal-input-wrapper {
display: flex;
align-items: stretch;
.edit-modal-input-field {
flex: 1;
display: flex;
align-items: center;
gap: var(--spacing-3);
height: 44px;
padding: 6px var(--padding-3);
background: var(--l1-background);
border: 1px solid var(--l1-border);
border-right: none;
border-radius: 2px 0 0 2px;
svg {
flex-shrink: 0;
}
input {
flex: 1;
width: 100%;
height: auto;
background: transparent;
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
color: var(--l1-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
padding: 0;
&:focus,
&:focus-visible {
outline: none;
box-shadow: none;
}
}
}
}
.edit-modal-input-suffix {
display: flex;
align-items: center;
padding: 6px var(--padding-3);
background: var(--l2-background);
border: 1px solid var(--l1-border);
border-left: none;
border-radius: 0 2px 2px 0;
color: var(--l2-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
white-space: nowrap;
}
.edit-modal-helper {
color: var(--l2-foreground);
font-size: var(--font-size-xs);
line-height: var(--line-height-20);
&--error {
color: var(--destructive);
}
}
.edit-modal-status-icon {
color: var(--l2-foreground);
}
.edit-modal-note {
display: flex;
gap: var(--spacing-6);
align-items: flex-start;
padding: var(--padding-3);
border-radius: 4px;
background: var(--l2-background);
}
.edit-modal-note-emoji {
font-size: 16px;
line-height: var(--line-height-20);
flex-shrink: 0;
margin-top: 2px;
}
.edit-modal-note-text {
color: var(--l2-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
.edit-modal-footer {
.facing-issue-button {
width: 100%;
.periscope-btn {
width: 100%;
border-radius: 2px;
background: var(--primary);
border: none;
color: var(--primary-foreground);
font-size: var(--paragraph-base-500-font-size);
font-weight: var(--paragraph-base-500-font-weight);
line-height: var(--paragraph-base-500-line-height);
height: 36px;
.ant-btn-icon {
display: none;
}
&:hover {
background: var(--primary) !important;
border: none !important;
color: var(--primary-foreground) !important;
}
}
}
}
.edit-modal-apply-btn {
width: 100%;
}
.custom-domain-settings-content {
width: calc(100% - 30px);
max-width: 736px;
.custom-domain-toast {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 0 16px;
height: 40px;
width: min(942px, calc(100vw - 32px));
border-radius: 4px;
background: var(--primary-background);
color: var(--bg-base-white);
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-lg);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 28px; /* 155.556% */
letter-spacing: -0.09px;
}
.custom-domain-toast-message {
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.subtitle {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.custom-domain-toast-actions {
display: flex;
align-items: center;
gap: var(--spacing-4);
flex-shrink: 0;
}
.custom-domain-toast-visit-btn {
text-decoration: none;
background: var(--bg-robin-600);
&:hover {
background: var(--primary-background-hover);
}
}
.custom-domain-settings-card {
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
.custom-domain-toast-dismiss-btn {
color: var(--callout-primary-icon);
height: 24px;
width: 24px;
.ant-card-body {
padding: 12px;
display: flex;
flex-direction: column;
.custom-domain-settings-content-header {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.custom-domain-settings-content-body {
margin-top: 12px;
display: flex;
gap: 12px;
align-items: flex-end;
justify-content: space-between;
.custom-domain-url-edit-btn {
.periscope-btn {
border-radius: 2px;
border: 1px solid var(--Slate-200, #2c3140);
background: var(--Ink-200, #23262e);
}
}
}
.custom-domain-urls {
display: flex;
flex-direction: column;
flex: 1;
}
.custom-domain-url {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
line-height: 24px;
padding: 4px 0;
}
.custom-domain-update-status {
margin-top: 12px;
color: var(--bg-robin-400);
font-size: 13px;
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
letter-spacing: -0.07px;
border-radius: 4px;
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
}
}
}
}
.custom-domain-settings-modal {
.ant-modal-content {
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
padding: 0;
.ant-modal-header {
background: none;
border-bottom: 1px solid var(--bg-slate-500);
padding: 16px;
margin-bottom: 0;
}
.ant-modal-close-x {
font-size: 12px;
}
.ant-modal-body {
padding: 12px 16px;
.custom-domain-settings-modal-body {
margin-bottom: 48px;
font-size: 13px;
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.custom-domain-settings-modal-error {
display: flex;
flex-direction: column;
gap: 24px;
.update-limit-reached-error {
display: flex;
padding: 20px 24px 24px 24px;
flex-direction: column;
align-items: center;
gap: 24px;
align-self: stretch;
border-radius: 4px;
border: 1px solid rgba(255, 205, 86, 0.2);
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-400);
font-size: 13px;
font-style: normal;
line-height: 20px; /* 142.857% */
}
.ant-alert-message::first-letter {
text-transform: capitalize;
}
}
.custom-domain-settings-modal-footer {
padding: 16px 0;
margin-top: 0;
display: flex;
justify-content: flex-end;
.apply-changes-btn {
width: 100%;
}
.facing-issue-button {
width: 100%;
.periscope-btn {
width: 100%;
border-radius: 2px;
background: var(--bg-robin-500);
border: none;
color: var(--bg-vanilla-100);
line-height: 20px;
.ant-btn-icon {
display: none;
}
&:hover {
background: var(--bg-robin-500) !important;
border: none !important;
color: var(--bg-vanilla-100) !important;
line-height: 20px !important;
}
}
}
}
}
}
.lightMode {
.custom-domain-settings-container {
.custom-domain-settings-content {
.title {
color: var(--bg-ink-400);
}
.subtitle {
color: var(--bg-vanilla-400);
}
}
.custom-domain-settings-card {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-card-body {
.custom-domain-settings-content-header {
color: var(--bg-ink-100);
}
.custom-domain-update-status {
color: var(--bg-robin-400);
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
}
.custom-domain-url-edit-btn {
.periscope-btn {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
box-shadow: none;
}
}
}
}
}
.custom-domain-settings-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
border-bottom: 1px solid var(--bg-vanilla-300);
}
.custom-domain-settings-modal-error {
.update-limit-reached-error {
border: 1px solid rgba(255, 205, 86, 0.2);
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-500);
}
}
&:hover {
background: var(--primary-background-hover);
}
}
}

View File

@@ -1,59 +1,88 @@
import { useEffect, useMemo, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { Color } from '@signozhq/design-tokens';
import { Button } from '@signozhq/button';
import { Callout } from '@signozhq/callout';
import {
Alert,
Button,
Card,
Form,
Input,
Modal,
Skeleton,
Tag,
Typography,
} from 'antd';
Check,
ChevronDown,
Clock,
ExternalLink,
FilePenLine,
Link2,
SolidAlertCircle,
X,
} from '@signozhq/icons';
import { toast } from '@signozhq/sonner';
import { Dropdown, Skeleton } from 'antd';
import {
RenderErrorResponseDTO,
ZeustypesHostDTO,
} from 'api/generated/services/sigNoz.schemas';
import { useGetHosts, usePutHost } from 'api/generated/services/zeus';
import { AxiosError } from 'axios';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useNotifications } from 'hooks/useNotifications';
import { InfoIcon, Link2, Pencil } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
import CustomDomainEditModal from './CustomDomainEditModal';
import './CustomDomainSettings.styles.scss';
interface CustomDomainSettingsProps {
subdomain: string;
function DomainUpdateToast({
toastId,
url,
}: {
toastId: string | number;
url: string;
}): JSX.Element {
const displayUrl = url?.split('://')[1] ?? url;
return (
<div className="custom-domain-toast">
<span className="custom-domain-toast-message">
Your workspace URL is being updated to <strong>{displayUrl}</strong>. This
may take a few minutes.
</span>
<div className="custom-domain-toast-actions">
<Button
variant="ghost"
size="xs"
className="custom-domain-toast-visit-btn"
suffixIcon={<ExternalLink size={12} />}
onClick={(): void => {
window.open(url, '_blank', 'noopener,noreferrer');
}}
>
Visit new URL
</Button>
<Button
variant="ghost"
size="icon"
className="custom-domain-toast-dismiss-btn"
onClick={(): void => {
toast.dismiss(toastId);
}}
aria-label="Dismiss"
prefixIcon={<X size={14} />}
/>
</div>
</div>
);
}
export default function CustomDomainSettings(): JSX.Element {
const { org } = useAppContext();
const { notifications } = useNotifications();
const { org, activeLicense } = useAppContext();
const { timezone } = useTimezone();
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isPollingEnabled, setIsPollingEnabled] = useState(false);
const [hosts, setHosts] = useState<ZeustypesHostDTO[] | null>(null);
const [updateDomainError, setUpdateDomainError] = useState<AxiosError | null>(
null,
);
const [, setCopyUrl] = useCopyToClipboard();
const [
customDomainDetails,
setCustomDomainDetails,
] = useState<CustomDomainSettingsProps | null>();
updateDomainError,
setUpdateDomainError,
] = useState<AxiosError<RenderErrorResponseDTO> | null>(null);
const [editForm] = Form.useForm();
const handleModalClose = (): void => {
setIsEditModalOpen(false);
editForm.resetFields();
setUpdateDomainError(null);
};
const [customDomainSubdomain, setCustomDomainSubdomain] = useState<
string | undefined
>();
const {
data: hostsData,
@@ -67,9 +96,7 @@ export default function CustomDomainSettings(): JSX.Element {
isLoading: isLoadingUpdateCustomDomain,
} = usePutHost<AxiosError<RenderErrorResponseDTO>>();
const stripProtocol = (url: string): string => {
return url?.split('://')[1] ?? url;
};
const stripProtocol = (url: string): string => url?.split('://')[1] ?? url;
const dnsSuffix = useMemo(() => {
const defaultHost = hosts?.find((h) => h.is_default);
@@ -78,6 +105,11 @@ export default function CustomDomainSettings(): JSX.Element {
: '';
}, [hosts]);
const activeHost = useMemo(
() => hosts?.find((h) => !h.is_default) ?? hosts?.find((h) => h.is_default),
[hosts],
);
useEffect(() => {
if (isFetchingHosts || !hostsData) {
return;
@@ -85,22 +117,14 @@ export default function CustomDomainSettings(): JSX.Element {
if (hostsData.status === 'success') {
setHosts(hostsData.data.hosts ?? null);
const activeCustomDomain = hostsData.data.hosts?.find(
(host) => !host.is_default,
);
if (activeCustomDomain) {
setCustomDomainDetails({
subdomain: activeCustomDomain?.name || '',
});
const customHost = hostsData.data.hosts?.find((h) => !h.is_default);
if (customHost) {
setCustomDomainSubdomain(customHost.name || '');
}
}
if (hostsData.data.state !== 'HEALTHY' && isPollingEnabled) {
setTimeout(() => {
refetchHosts();
}, 3000);
setTimeout(() => refetchHosts(), 3000);
}
if (hostsData.data.state === 'HEALTHY') {
@@ -108,206 +132,175 @@ export default function CustomDomainSettings(): JSX.Element {
}
}, [hostsData, refetchHosts, isPollingEnabled, isFetchingHosts]);
const onUpdateCustomDomainSettings = (): void => {
editForm
.validateFields()
.then((values) => {
if (values.subdomain) {
updateSubDomain(
{ data: { name: values.subdomain } },
{
onSuccess: () => {
setIsPollingEnabled(true);
refetchHosts();
setIsEditModalOpen(false);
},
onError: (error: AxiosError<RenderErrorResponseDTO>) => {
setUpdateDomainError(error as AxiosError);
setIsPollingEnabled(false);
},
},
const handleSubmit = (subdomain: string): void => {
updateSubDomain(
{ data: { name: subdomain } },
{
onSuccess: () => {
setIsPollingEnabled(true);
refetchHosts();
setIsEditModalOpen(false);
setCustomDomainSubdomain(subdomain);
const newUrl = `https://${subdomain}.${dnsSuffix}`;
toast.custom(
(toastId) => <DomainUpdateToast toastId={toastId} url={newUrl} />,
{ duration: 5000, position: 'bottom-right' }, // this 5 sec is as per design
);
},
onError: (error: AxiosError<RenderErrorResponseDTO>) => {
setUpdateDomainError(error as AxiosError<RenderErrorResponseDTO>);
setIsPollingEnabled(false);
},
},
);
};
setCustomDomainDetails({
subdomain: values.subdomain,
});
const sortedHosts = useMemo(
() =>
[...(hosts ?? [])].sort((a, b) => {
if (a.name === activeHost?.name) {
return -1;
}
})
.catch((errorInfo) => {
console.error('error info', errorInfo);
});
};
if (b.name === activeHost?.name) {
return 1;
}
if (a.is_default && !b.is_default) {
return 1;
}
if (!a.is_default && b.is_default) {
return -1;
}
return 0;
}),
[hosts, activeHost],
);
const onCopyUrlHandler = (url: string): void => {
setCopyUrl(stripProtocol(url));
notifications.success({
message: 'Copied to clipboard',
});
};
const planName = activeLicense?.plan?.name;
if (isLoadingHosts) {
return (
<div className="custom-domain-card custom-domain-card--loading">
<Skeleton
active
title={{ width: '40%' }}
paragraph={{ rows: 1, width: '60%' }}
/>
</div>
);
}
return (
<div className="custom-domain-settings-container">
<div className="custom-domain-settings-content">
<header>
<Typography.Title className="title">
Custom Domain Settings
</Typography.Title>
<Typography.Text className="subtitle">
Personalize your workspace domain effortlessly.
</Typography.Text>
</header>
</div>
<div className="custom-domain-settings-content">
{!isLoadingHosts && (
<Card className="custom-domain-settings-card">
<div className="custom-domain-settings-content-header">
Team {org?.[0]?.displayName} Information
<>
<div className="custom-domain-card">
<div className="custom-domain-card-top">
<div className="custom-domain-card-info">
<div className="custom-domain-card-name-row">
<span className="beacon" />
<span className="custom-domain-card-org-name">
{org?.[0]?.displayName ? org?.[0]?.displayName : customDomainSubdomain}
</span>
</div>
<div className="custom-domain-settings-content-body">
<div className="custom-domain-urls">
{hosts?.map((host) => (
<div
className="custom-domain-url"
key={host.name}
onClick={(): void => onCopyUrlHandler(host.url || '')}
>
<Link2 size={12} /> {stripProtocol(host.url || '')}
{host.is_default && <Tag color={Color.BG_ROBIN_500}>Default</Tag>}
<div className="custom-domain-card-meta-row">
<Dropdown
trigger={['click']}
dropdownRender={(): JSX.Element => (
<div className="workspace-url-dropdown">
<span className="workspace-url-dropdown-header">
All Workspace URLs
</span>
<div className="workspace-url-dropdown-divider" />
{sortedHosts.map((host) => {
const isActive = host.name === activeHost?.name;
return (
<a
key={host.name}
href={host.url}
target="_blank"
rel="noopener noreferrer"
className={`workspace-url-dropdown-item${
isActive ? ' workspace-url-dropdown-item--active' : ''
}`}
>
<span className="workspace-url-dropdown-item-label">
{stripProtocol(host.url ?? '')}
</span>
{isActive ? (
<Check size={14} className="workspace-url-dropdown-item-check" />
) : (
<ExternalLink
size={12}
className="workspace-url-dropdown-item-external"
/>
)}
</a>
);
})}
</div>
))}
</div>
<div className="custom-domain-url-edit-btn">
)}
>
<Button
className="periscope-btn"
disabled={isLoadingHosts || isFetchingHosts || isPollingEnabled}
type="default"
icon={<Pencil size={10} />}
onClick={(): void => setIsEditModalOpen(true)}
type="button"
size="xs"
className="workspace-url-trigger"
disabled={isFetchingHosts}
>
Customize teams URL
<Link2 size={12} />
<span>{stripProtocol(activeHost?.url ?? '')}</span>
<ChevronDown size={12} />
</Button>
</div>
</Dropdown>
<span className="custom-domain-card-meta-timezone">
<Clock size={11} />
{timezone.offset}
</span>
</div>
</div>
{isPollingEnabled && (
<Alert
className="custom-domain-update-status"
message={`Updating your URL to ⎯ ${customDomainDetails?.subdomain}.${dnsSuffix}. This may take a few mins.`}
type="info"
icon={<InfoIcon size={12} />}
/>
)}
</Card>
<Button
variant="solid"
size="sm"
className="custom-domain-edit-button"
prefixIcon={<FilePenLine size={12} />}
disabled={isFetchingHosts || isPollingEnabled}
onClick={(): void => setIsEditModalOpen(true)}
>
Edit workspace link
</Button>
</div>
{isPollingEnabled && (
<Callout
type="info"
showIcon
className="custom-domain-callout"
size="small"
icon={<SolidAlertCircle size={13} color="primary" />}
message={`Updating your URL to ⎯ ${customDomainSubdomain}.${dnsSuffix}. This may take a few mins.`}
/>
)}
{isLoadingHosts && (
<Card className="custom-domain-settings-card">
<Skeleton
className="custom-domain-settings-skeleton"
active
paragraph={{ rows: 2 }}
/>
</Card>
)}
<div className="custom-domain-card-divider" />
<div className="custom-domain-card-bottom">
<span className="beacon" />
<span className="custom-domain-card-license">
{planName && <code className="custom-domain-plan-badge">{planName}</code>}{' '}
license is currently active
</span>
</div>
</div>
{/* Update Custom Domain Modal */}
<Modal
className="custom-domain-settings-modal"
title="Customize your teams URL"
open={isEditModalOpen}
key="edit-custom-domain-settings-modal"
afterClose={handleModalClose}
// closable
onCancel={handleModalClose}
destroyOnClose
footer={null}
>
<Form
name="edit-custom-domain-settings-form"
key={customDomainDetails?.subdomain}
form={editForm}
layout="vertical"
autoComplete="off"
initialValues={{
subdomain: customDomainDetails?.subdomain,
}}
>
{updateDomainError?.status !== 409 && (
<>
<div className="custom-domain-settings-modal-body">
Enter your preferred subdomain to create a unique URL for your team.
Need help? Contact support.
</div>
<Form.Item
name="subdomain"
label="Teams URL subdomain"
rules={[{ required: true }, { type: 'string', min: 3 }]}
>
<Input
addonBefore={updateDomainError && <InfoIcon size={12} color="red" />}
placeholder="Enter Domain"
onChange={(): void => setUpdateDomainError(null)}
addonAfter={dnsSuffix}
autoFocus
/>
</Form.Item>
</>
)}
{updateDomainError && (
<div className="custom-domain-settings-modal-error">
{updateDomainError.status === 409 ? (
<Alert
message={
(updateDomainError?.response?.data as RenderErrorResponseDTO)?.error
?.message ||
'Youve already updated the custom domain once today. To make further changes, please contact our support team for assistance.'
}
type="warning"
className="update-limit-reached-error"
/>
) : (
<Typography.Text type="danger">
{
(updateDomainError?.response?.data as RenderErrorResponseDTO)?.error
?.message
}
</Typography.Text>
)}
</div>
)}
{updateDomainError?.status !== 409 && (
<div className="custom-domain-settings-modal-footer">
<Button
className="periscope-btn primary apply-changes-btn"
onClick={onUpdateCustomDomainSettings}
loading={isLoadingUpdateCustomDomain}
>
Apply Changes
</Button>
</div>
)}
{updateDomainError?.status === 409 && (
<div className="custom-domain-settings-modal-footer">
<LaunchChatSupport
attributes={{
screen: 'Custom Domain Settings',
}}
eventName="Custom Domain Settings: Facing Issues Updating Custom Domain"
message="Hi Team, I need help with updating custom domain"
buttonText="Contact Support"
/>
</div>
)}
</Form>
</Modal>
</div>
<CustomDomainEditModal
isOpen={isEditModalOpen}
onClose={(): void => setIsEditModalOpen(false)}
customDomainSubdomain={customDomainSubdomain}
dnsSuffix={dnsSuffix}
isLoading={isLoadingUpdateCustomDomain}
updateDomainError={updateDomainError}
onClearError={(): void => setUpdateDomainError(null)}
onSubmit={handleSubmit}
/>
</>
);
}

View File

@@ -4,6 +4,21 @@ import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import CustomDomainSettings from '../CustomDomainSettings';
jest.mock('components/LaunchChatSupport/LaunchChatSupport', () => ({
__esModule: true,
default: ({ buttonText }: { buttonText?: string }): JSX.Element => (
<button type="button">{buttonText ?? 'Facing issues?'}</button>
),
}));
const mockToastCustom = jest.fn();
jest.mock('@signozhq/sonner', () => ({
toast: {
custom: (...args: unknown[]): unknown => mockToastCustom(...args),
dismiss: jest.fn(),
},
}));
const ZEUS_HOSTS_ENDPOINT = '*/api/v2/zeus/hosts';
const mockHostsResponse: GetHosts200 = {
@@ -28,9 +43,12 @@ const mockHostsResponse: GetHosts200 = {
};
describe('CustomDomainSettings', () => {
afterEach(() => server.resetHandlers());
afterEach(() => {
server.resetHandlers();
mockToastCustom.mockClear();
});
it('renders host URLs with protocol stripped and marks the default host', async () => {
it('renders active host URL in the trigger button', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
@@ -39,12 +57,11 @@ describe('CustomDomainSettings', () => {
render(<CustomDomainSettings />);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
// The active host is the non-default one (custom-host)
await screen.findByText(/custom-host\.test\.cloud/i);
expect(screen.getByText('Default')).toBeInTheDocument();
});
it('opens edit modal with DNS suffix derived from the default host', async () => {
it('opens edit modal when clicking the edit button', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
@@ -54,14 +71,14 @@ describe('CustomDomainSettings', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /customize team[']s url/i }),
screen.getByRole('button', { name: /edit workspace link/i }),
);
expect(
screen.getByRole('dialog', { name: /customize your team[']s url/i }),
screen.getByRole('dialog', { name: /edit workspace link/i }),
).toBeInTheDocument();
// DNS suffix is the part of the default host URL after the name prefix
expect(screen.getByText('test.cloud')).toBeInTheDocument();
@@ -83,12 +100,13 @@ describe('CustomDomainSettings', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /customize team[']s url/i }),
screen.getByRole('button', { name: /edit workspace link/i }),
);
const input = screen.getByPlaceholderText(/enter domain/i);
// The input is inside the modal — find it by its role
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
@@ -114,15 +132,111 @@ describe('CustomDomainSettings', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /customize team[']s url/i }),
screen.getByRole('button', { name: /edit workspace link/i }),
);
await user.type(screen.getByPlaceholderText(/enter domain/i), 'myteam');
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
expect(
await screen.findByRole('button', { name: /contact support/i }),
).toBeInTheDocument();
});
it('shows validation error when subdomain is less than 3 characters', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'ab');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
expect(
screen.getByText(/minimum 3 characters required/i),
).toBeInTheDocument();
});
it('shows all workspace URLs as links in the dropdown', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
// Open the URL dropdown
await user.click(
screen.getByRole('button', { name: /custom-host\.test\.cloud/i }),
);
// Both host URLs should appear as links in the dropdown
const links = await screen.findAllByRole('link');
const hostLinks = links.filter(
(link) =>
link.getAttribute('href')?.includes('test.cloud') &&
link.getAttribute('target') === '_blank',
);
expect(hostLinks).toHaveLength(2);
// Verify the URLs
const hrefs = hostLinks.map((link) => link.getAttribute('href'));
expect(hrefs).toContain('https://accepted-starfish.test.cloud');
expect(hrefs).toContain('https://custom-host.test.cloud');
});
it('calls toast.custom with new URL after successful domain update', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
rest.put(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json({})),
),
);
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<CustomDomainSettings />);
await screen.findByText(/custom-host\.test\.cloud/i);
await user.click(
screen.getByRole('button', { name: /edit workspace link/i }),
);
const input = screen.getByRole('textbox');
await user.clear(input);
await user.type(input, 'myteam');
await user.click(screen.getByRole('button', { name: /apply changes/i }));
// Verify toast.custom was called
await waitFor(() => {
expect(mockToastCustom).toHaveBeenCalledTimes(1);
});
// Render the toast element to verify its content
const toastRenderer = mockToastCustom.mock.calls[0][0] as (
id: string,
) => JSX.Element;
const { container } = render(toastRenderer('test-id'));
expect(container).toHaveTextContent(/myteam\.test\.cloud/i);
});
});

View File

@@ -11,8 +11,7 @@ import {
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Modal, Row, Space, Table, Typography } from 'antd';
import { RowProps } from 'antd/lib';
import { Button, Modal, Row, RowProps, Space, Table, Typography } from 'antd';
import { VariablesSettingsTabHandle } from 'container/DashboardContainer/DashboardDescription/types';
import { convertVariablesToDbFormat } from 'container/DashboardContainer/DashboardVariablesSelection/util';
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';

View File

@@ -1,4 +1,4 @@
import { ColumnType } from 'antd/es/table';
import { TableColumnType as ColumnType } from 'antd';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import CustomCheckBox from 'container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox';

View File

@@ -1,6 +1,6 @@
import { Dispatch, SetStateAction } from 'react';
import { UseMutateAsyncFunction } from 'react-query';
import { NotificationInstance } from 'antd/es/notification/interface';
import type { NotificationInstance } from 'antd/es/notification/interface';
import { AxiosResponse } from 'axios';
import { SaveViewWithNameProps } from 'components/ExplorerCard/types';
import { PANEL_TYPES } from 'constants/queryBuilder';

View File

@@ -1,7 +1,7 @@
import { Dispatch, ReactElement, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { Form, FormInstance, Input, Select, Switch, Typography } from 'antd';
import { Store } from 'antd/lib/form/interface';
import type { Store } from 'antd/lib/form/interface';
import ROUTES from 'constants/routes';
import {
ChannelType,

View File

@@ -10,7 +10,7 @@ import {
Space,
Typography,
} from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import type { DefaultOptionType } from 'antd/es/select';
import {
getCategoryByOptionId,
getCategorySelectOptionByName,

View File

@@ -0,0 +1,152 @@
.general-settings-page {
max-width: 768px;
margin: 0 auto;
padding: var(--padding-8) 0 var(--padding-16);
display: flex;
flex-direction: column;
gap: var(--spacing-12);
}
.general-settings-header {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
}
.general-settings-title {
font-size: var(--paragraph-medium-500-font-size);
font-weight: var(--paragraph-medium-500-font-weight);
line-height: 32px;
letter-spacing: -0.08px;
color: var(--l1-foreground);
}
.general-settings-subtitle {
font-size: var(--font-size-sm);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
color: var(--l2-foreground);
}
.retention-controls-container {
border: 1px solid var(--l2-border);
border-radius: 4px;
overflow: hidden;
}
.retention-controls-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--padding-4);
height: 50px;
background: var(--l2-background);
border-bottom: 1px solid var(--l2-border);
}
.retention-controls-header-label {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-24);
letter-spacing: 0.48px;
text-transform: uppercase;
color: var(--l2-foreground);
}
.retention-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--padding-4);
height: 52px;
background: var(--l2-background);
& + & {
border-top: 1px solid var(--l1-border);
}
}
.retention-row-label {
display: flex;
align-items: center;
gap: var(--spacing-3);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
color: var(--l1-foreground);
svg {
color: var(--l2-foreground);
flex-shrink: 0;
}
}
.retention-row-controls {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
.retention-input-group {
display: flex;
align-items: flex-start;
// todo: https://github.com/SigNoz/components/issues/116
input[type='number'] {
display: flex;
width: 120px;
height: 32px;
align-items: center;
gap: var(--spacing-2);
border-radius: 2px 0 0 2px;
border: 1px solid var(--l2-border);
background: transparent;
text-align: left;
-moz-appearance: textfield;
appearance: textfield;
color: var(--l1-foreground);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-normal);
line-height: 16px;
box-shadow: none;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
&:disabled {
opacity: 0.8;
cursor: not-allowed;
}
}
.ant-select {
.ant-select-selector {
display: flex;
height: 32px;
padding: 6px 6px 6px 8px;
align-items: center;
gap: var(--spacing-2);
border: 1px solid var(--l2-border);
background: var(--l2-background);
width: 80px;
border-left: none;
}
}
}
.retention-error-text {
font-size: var(--font-size-xs);
color: var(--accent-amber);
font-style: italic;
}
.retention-modal-description {
margin: 0;
color: var(--l1-foreground);
font-size: var(--font-size-sm);
line-height: 22px;
}

View File

@@ -3,16 +3,20 @@ import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { useInterval } from 'react-use';
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Card, Col, Divider, Modal, Row, Spin, Typography } from 'antd';
import { Button } from '@signozhq/button';
import { Compass, ScrollText } from '@signozhq/icons';
import { Modal, Spin } from 'antd';
import setRetentionApi from 'api/settings/setRetention';
import setRetentionApiV2 from 'api/settings/setRetentionV2';
import TextToolTip from 'components/TextToolTip';
import CustomDomainSettings from 'container/CustomDomainSettings';
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
import { StatusCodes } from 'http-status-codes';
import find from 'lodash-es/find';
import { BarChart2 } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import {
ErrorResponse,
@@ -31,13 +35,17 @@ import {
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
} from 'types/api/settings/getRetention';
import { USER_ROLES } from 'types/roles';
import Retention from './Retention';
import StatusMessage from './StatusMessage';
import { ActionItemsContainer, ErrorText, ErrorTextContainer } from './styles';
import './GeneralSettings.styles.scss';
type NumberOrNull = number | null;
// eslint-disable-next-line sonarjs/cognitive-complexity
function GeneralSettings({
metricsTtlValuesPayload,
tracesTtlValuesPayload,
@@ -455,12 +463,20 @@ function GeneralSettings({
onModalToggleHandler(type);
};
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const {
isCloudUser: isCloudUserVal,
isEnterpriseSelfHostedUser,
} = useGetTenantLicense();
const isAdmin = user.role === USER_ROLES.ADMIN;
const showCustomDomainSettings =
(isCloudUserVal || isEnterpriseSelfHostedUser) && isAdmin;
const renderConfig = [
{
name: 'Metrics',
type: 'metrics',
icon: <BarChart2 size={14} />,
retentionFields: [
{
name: t('total_retention_period'),
@@ -503,6 +519,7 @@ function GeneralSettings({
{
name: 'Traces',
type: 'traces',
icon: <Compass size={14} />,
retentionFields: [
{
name: t('total_retention_period'),
@@ -543,6 +560,7 @@ function GeneralSettings({
{
name: 'Logs',
type: 'logs',
icon: <ScrollText size={14} />,
retentionFields: [
{
name: t('total_retention_period'),
@@ -587,69 +605,66 @@ function GeneralSettings({
) {
return (
<Fragment key={category.name}>
<Col xs={22} xl={11} key={category.name} style={{ margin: '0.5rem' }}>
<Card style={{ height: '100%' }}>
<Typography.Title style={{ margin: 0 }} level={3}>
{category.name}
</Typography.Title>
<Divider
style={{
margin: '0.5rem 0',
padding: 0,
opacity: 0.5,
marginBottom: '1rem',
}}
/>
{category.retentionFields.map((retentionField) => (
<div className="retention-row">
<span className="retention-row-label">
{category.icon}
{category.name}
</span>
<div className="retention-row-controls">
{category.retentionFields.map((field) => (
<Retention
key={field.name}
type={category.type as TTTLType}
key={retentionField.name}
text={retentionField.name}
retentionValue={retentionField.value}
setRetentionValue={retentionField.setValue}
hide={!!retentionField.hide}
isS3Field={'isS3Field' in retentionField && retentionField.isS3Field}
text={field.name}
retentionValue={field.value}
setRetentionValue={field.setValue}
hide={!!field.hide}
isS3Field={'isS3Field' in field && !!field.isS3Field}
compact
/>
))}
{!isCloudUserVal && (
<>
<ActionItemsContainer>
<Button
type="primary"
onClick={category.save.modalOpen}
disabled={category.save.isDisabled}
>
{category.save.saveButtonText}
</Button>
{category.statusComponent}
</ActionItemsContainer>
<Modal
title={t('retention_confirmation')}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={(): void =>
onModalToggleHandler(category.name.toLowerCase() as TTTLType)
}
onOk={(): Promise<void> =>
onOkHandler(category.name.toLowerCase() as TTTLType)
}
centered
open={category.save.modal}
confirmLoading={category.save.apiLoading}
>
<Typography>
{t('retention_confirmation_description', {
name: category.name.toLowerCase(),
})}
</Typography>
</Modal>
</>
<Button
variant="solid"
size="sm"
color="primary"
onClick={category.save.modalOpen}
disabled={category.save.isDisabled}
>
{category.save.saveButtonText}
</Button>
)}
</Card>
</Col>
</div>
</div>
{!isCloudUserVal && (
<ActionItemsContainer>{category.statusComponent}</ActionItemsContainer>
)}
{!isCloudUserVal && (
<Modal
title={t('retention_confirmation')}
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={(): void =>
onModalToggleHandler(category.name.toLowerCase() as TTTLType)
}
onOk={(): Promise<void> =>
onOkHandler(category.name.toLowerCase() as TTTLType)
}
centered
open={category.save.modal}
confirmLoading={category.save.apiLoading}
>
<p className="retention-modal-description">
{t('retention_confirmation_description', {
name: category.name.toLowerCase(),
})}
</p>
</Modal>
)}
</Fragment>
);
}
@@ -657,9 +672,24 @@ function GeneralSettings({
});
return (
<>
{Element}
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
<div className="general-settings-page">
<div className="general-settings-header">
<span className="general-settings-title">General</span>
<span className="general-settings-subtitle">
Manage your workspace settings.
</span>
</div>
{showCustomDomainSettings && <CustomDomainSettings />}
<div className="retention-controls-container">
<div className="retention-controls-header">
<span className="retention-controls-header-label">Retention Controls</span>
</div>
{renderConfig}
</div>
{(!isCloudUserVal || errorText) && (
<ErrorTextContainer>
{!isCloudUserVal && (
<TextToolTip
@@ -671,12 +701,10 @@ function GeneralSettings({
)}
{errorText && <ErrorText>{errorText}</ErrorText>}
</ErrorTextContainer>
)}
<Row justify="start">{renderConfig}</Row>
{isCloudUserVal && <GeneralSettingsCloud />}
</Col>
</>
{isCloudUserVal && <GeneralSettingsCloud />}
</div>
);
}

View File

@@ -7,6 +7,7 @@ import {
useRef,
useState,
} from 'react';
import { Input as SignozInput } from '@signozhq/input';
import { Col, Row, Select } from 'antd';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { find } from 'lodash-es';
@@ -34,6 +35,7 @@ function Retention({
text,
hide,
isS3Field = false,
compact = false,
}: RetentionProps): JSX.Element | null {
// Filter available units based on type and field
const availableUnits = useMemo(
@@ -126,6 +128,27 @@ function Retention({
return null;
}
if (compact) {
return (
<div className="retention-input-group">
<SignozInput
type="number"
min={0}
value={selectedValue && selectedValue >= 0 ? selectedValue : ''}
disabled={isCloudUserVal}
onChange={(e): void => onChangeHandler(e, setSelectedValue)}
/>
<Select
value={selectedTimeUnit}
onChange={currentSelectedOption}
disabled={isCloudUserVal}
>
{menuItems}
</Select>
</div>
);
}
return (
<RetentionContainer>
<Row justify="space-between">
@@ -162,9 +185,11 @@ interface RetentionProps {
setRetentionValue: Dispatch<SetStateAction<number | null>>;
hide: boolean;
isS3Field?: boolean;
compact?: boolean;
}
Retention.defaultProps = {
isS3Field: false,
compact: false,
};
export default Retention;

View File

@@ -1,4 +1,5 @@
import setRetentionApiV2 from 'api/settings/setRetentionV2';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import {
fireEvent,
render,
@@ -35,14 +36,20 @@ jest.mock('hooks/useComponentPermission', () => ({
}));
jest.mock('hooks/useGetTenantLicense', () => ({
useGetTenantLicense: (): { isCloudUser: boolean } => ({
useGetTenantLicense: jest.fn(() => ({
isCloudUser: false,
}),
isEnterpriseSelfHostedUser: false,
})),
}));
jest.mock('container/GeneralSettingsCloud', () => ({
__esModule: true,
default: (): null => null,
default: (): JSX.Element => <div data-testid="general-settings-cloud" />,
}));
jest.mock('container/CustomDomainSettings', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="custom-domain-settings" />,
}));
// Mock data
@@ -93,10 +100,12 @@ const mockDisksWithoutS3: IDiskType[] = [
},
];
describe('GeneralSettings - S3 Logs Retention', () => {
const BUTTON_SELECTOR = 'button[type="button"]';
const PRIMARY_BUTTON_CLASS = 'ant-btn-primary';
const getLogsRow = (): HTMLElement => {
const logsLabel = screen.getByText('Logs');
return logsLabel.closest('.retention-row') as HTMLElement;
};
describe('GeneralSettings - S3 Logs Retention', () => {
beforeEach(() => {
jest.clearAllMocks();
(setRetentionApiV2 as jest.Mock).mockResolvedValue({
@@ -121,21 +130,20 @@ describe('GeneralSettings - S3 Logs Retention', () => {
/>,
);
// Find the Logs card
const logsCard = screen.getByText('Logs').closest('.ant-card');
expect(logsCard).toBeInTheDocument();
const logsRow = getLogsRow();
expect(logsRow).toBeInTheDocument();
// Find all inputs in the Logs card - there should be 2 (total retention + S3)
const inputs = logsCard?.querySelectorAll('input[type="text"]');
// Find all inputs in the Logs row - there should be 2 (total retention + S3)
const inputs = logsRow.querySelectorAll('input[type="number"]');
expect(inputs).toHaveLength(2);
// The second input is the S3 retention field
const s3Input = inputs?.[1] as HTMLInputElement;
const s3Input = inputs[1] as HTMLInputElement;
// Find the S3 dropdown (next sibling of the S3 input)
const s3Dropdown = s3Input?.nextElementSibling?.querySelector(
'.ant-select-selector',
) as HTMLElement;
const s3Dropdown = s3Input
?.closest('.retention-row-controls')
?.querySelectorAll('.ant-select-selector')[1] as HTMLElement;
expect(s3Dropdown).toBeInTheDocument();
// Click the S3 dropdown to open it
@@ -155,16 +163,13 @@ describe('GeneralSettings - S3 Logs Retention', () => {
await user.clear(s3Input);
await user.type(s3Input, '5');
// Find the save button in the Logs card
const buttons = logsCard?.querySelectorAll(BUTTON_SELECTOR);
// The primary button should be the save button
const saveButton = Array.from(buttons || []).find((btn) =>
btn.className.includes(PRIMARY_BUTTON_CLASS),
// Find the save button in the Logs row
const saveButton = logsRow.querySelector(
'button:not([disabled])',
) as HTMLButtonElement;
expect(saveButton).toBeInTheDocument();
// Wait for button to be enabled (it should enable after value changes)
// Wait for button to be enabled
await waitFor(() => {
expect(saveButton).not.toBeDisabled();
});
@@ -205,8 +210,8 @@ describe('GeneralSettings - S3 Logs Retention', () => {
);
// Verify S3 field is visible
const logsCard = screen.getByText('Logs').closest('.ant-card');
const inputs = logsCard?.querySelectorAll('input[type="text"]');
const logsRow = getLogsRow();
const inputs = logsRow.querySelectorAll('input[type="number"]');
expect(inputs).toHaveLength(2); // Total + S3
});
});
@@ -227,19 +232,18 @@ describe('GeneralSettings - S3 Logs Retention', () => {
/>,
);
// Find the Logs card
const logsCard = screen.getByText('Logs').closest('.ant-card');
expect(logsCard).toBeInTheDocument();
const logsRow = getLogsRow();
expect(logsRow).toBeInTheDocument();
// Only 1 input should be visible (total retention, no S3)
const inputs = logsCard?.querySelectorAll('input[type="text"]');
const inputs = logsRow.querySelectorAll('input[type="number"]');
expect(inputs).toHaveLength(1);
// Change total retention value
const totalInput = inputs?.[0] as HTMLInputElement;
const totalInput = inputs[0] as HTMLInputElement;
// First, change the dropdown to Days (it defaults to Months)
const totalDropdown = totalInput?.nextElementSibling?.querySelector(
const totalDropdown = logsRow.querySelector(
'.ant-select-selector',
) as HTMLElement;
await user.click(totalDropdown);
@@ -263,14 +267,12 @@ describe('GeneralSettings - S3 Logs Retention', () => {
await user.type(totalInput, '60');
// Find the save button
const buttons = logsCard?.querySelectorAll(BUTTON_SELECTOR);
const saveButton = Array.from(buttons || []).find((btn) =>
btn.className.includes(PRIMARY_BUTTON_CLASS),
const saveButton = logsRow.querySelector(
'button:not([disabled])',
) as HTMLButtonElement;
expect(saveButton).toBeInTheDocument();
// Wait for button to be enabled (ensures all state updates have settled)
// Wait for button to be enabled
await waitFor(() => {
expect(saveButton).not.toBeDisabled();
});
@@ -312,22 +314,21 @@ describe('GeneralSettings - S3 Logs Retention', () => {
/>,
);
// Find the Logs card
const logsCard = screen.getByText('Logs').closest('.ant-card');
const inputs = logsCard?.querySelectorAll('input[type="text"]');
const logsRow = getLogsRow();
const inputs = logsRow.querySelectorAll('input[type="number"]');
// Total retention: 720 hours = 30 days = 1 month (displays as 1 Month)
const totalInput = inputs?.[0] as HTMLInputElement;
// Total retention: 30 days = 1 month (displays as 1 Month)
const totalInput = inputs[0] as HTMLInputElement;
expect(totalInput.value).toBe('1');
// S3 retention: 24 day
const s3Input = inputs?.[1] as HTMLInputElement;
// S3 retention: 24 days
const s3Input = inputs[1] as HTMLInputElement;
expect(s3Input.value).toBe('24');
// Verify dropdowns: total shows Months, S3 shows Days
const dropdowns = logsCard?.querySelectorAll('.ant-select-selection-item');
expect(dropdowns?.[0]).toHaveTextContent('Months');
expect(dropdowns?.[1]).toHaveTextContent('Days');
const dropdowns = logsRow.querySelectorAll('.ant-select-selection-item');
expect(dropdowns[0]).toHaveTextContent('Months');
expect(dropdowns[1]).toHaveTextContent('Days');
});
});
@@ -347,24 +348,22 @@ describe('GeneralSettings - S3 Logs Retention', () => {
/>,
);
// Find the Logs card
const logsCard = screen.getByText('Logs').closest('.ant-card');
expect(logsCard).toBeInTheDocument();
const logsRow = getLogsRow();
expect(logsRow).toBeInTheDocument();
// Find the save button
const buttons = logsCard?.querySelectorAll(BUTTON_SELECTOR);
const saveButton = Array.from(buttons || []).find((btn) =>
btn.className.includes(PRIMARY_BUTTON_CLASS),
// Find the save button by accessible name within the Logs row
const allSaveButtons = screen.getAllByRole('button', { name: /save/i });
const saveButton = allSaveButtons.find((btn) =>
logsRow.contains(btn),
) as HTMLButtonElement;
expect(saveButton).toBeInTheDocument();
// Verify save button is disabled on initial load (no changes, S3 disabled with -1)
// Verify save button is disabled on initial load
expect(saveButton).toBeDisabled();
// Find the total retention input
const inputs = logsCard?.querySelectorAll('input[type="text"]');
const totalInput = inputs?.[0] as HTMLInputElement;
const inputs = logsRow.querySelectorAll('input[type="number"]');
const totalInput = inputs[0] as HTMLInputElement;
// Change total retention value to trigger button enable
await user.clear(totalInput);
@@ -385,4 +384,62 @@ describe('GeneralSettings - S3 Logs Retention', () => {
});
});
});
describe('Cloud User Rendering', () => {
beforeEach(() => {
(useGetTenantLicense as jest.Mock).mockReturnValue({
isCloudUser: true,
isEnterpriseSelfHostedUser: false,
});
});
it('should render CustomDomainSettings and GeneralSettingsCloud for cloud admin', () => {
render(
<GeneralSettings
metricsTtlValuesPayload={mockMetricsRetention}
tracesTtlValuesPayload={mockTracesRetention}
logsTtlValuesPayload={mockLogsRetentionWithS3}
getAvailableDiskPayload={mockDisksWithS3}
metricsTtlValuesRefetch={jest.fn()}
tracesTtlValuesRefetch={jest.fn()}
logsTtlValuesRefetch={jest.fn()}
/>,
);
expect(screen.getByTestId('custom-domain-settings')).toBeInTheDocument();
expect(screen.getByTestId('general-settings-cloud')).toBeInTheDocument();
});
});
describe('Enterprise Self-Hosted User Rendering', () => {
beforeEach(() => {
(useGetTenantLicense as jest.Mock).mockReturnValue({
isCloudUser: false,
isEnterpriseSelfHostedUser: true,
});
});
it('should render CustomDomainSettings but not GeneralSettingsCloud', () => {
render(
<GeneralSettings
metricsTtlValuesPayload={mockMetricsRetention}
tracesTtlValuesPayload={mockTracesRetention}
logsTtlValuesPayload={mockLogsRetentionWithS3}
getAvailableDiskPayload={mockDisksWithS3}
metricsTtlValuesRefetch={jest.fn()}
tracesTtlValuesRefetch={jest.fn()}
logsTtlValuesRefetch={jest.fn()}
/>,
);
expect(screen.getByTestId('custom-domain-settings')).toBeInTheDocument();
expect(
screen.queryByTestId('general-settings-cloud'),
).not.toBeInTheDocument();
// Save buttons should be visible for self-hosted
const saveButtons = screen.getAllByRole('button', { name: /save/i });
expect(saveButtons.length).toBeGreaterThan(0);
});
});
});

View File

@@ -1,11 +1,11 @@
.general-settings-container {
margin: 16px 8px;
margin: var(--spacing-8) 0px;
.ant-card-body {
display: flex;
align-items: center;
gap: 16px;
gap: var(--spacing-8);
padding: 8px;
margin: 16px 0;
margin: var(--spacing-8) 0;
}
}

View File

@@ -1,6 +1,6 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { Button, Input } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ResizeTable } from 'components/ResizeTable';
import { useNotifications } from 'hooks/useNotifications';
import { useDashboard } from 'providers/Dashboard/Dashboard';

View File

@@ -1,6 +1,6 @@
import { grey } from '@ant-design/colors';
import { Checkbox, ConfigProvider } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import { CheckBoxProps } from '../types';

View File

@@ -1,4 +1,4 @@
import { ColumnType } from 'antd/es/table';
import { TableColumnType as ColumnType } from 'antd';
import { DataSetProps } from '../types';
import Label from './Label';

View File

@@ -1,5 +1,5 @@
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ColumnType } from 'antd/es/table';
import { TableColumnType as ColumnType } from 'antd';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ColumnsKeyAndDataIndex, ColumnsTitle } from '../contants';
import { DataSetProps, ExtendedChartDataset } from '../types';

View File

@@ -1,5 +1,5 @@
import { Dispatch, MutableRefObject, RefObject, SetStateAction } from 'react';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ToggleGraphProps } from 'components/Graph/types';
import { UplotProps } from 'components/Uplot/Uplot';
import { PANEL_TYPES } from 'constants/queryBuilder';

View File

@@ -1,5 +1,5 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { NotificationInstance } from 'antd/es/notification/interface';
import type { NotificationInstance } from 'antd/es/notification/interface';
import { NavigateToExplorerProps } from 'components/CeleryTask/useNavigateToExplorer';
import { LOCALSTORAGE } from 'constants/localStorage';
import { PANEL_TYPES } from 'constants/queryBuilder';

View File

@@ -7,7 +7,6 @@ import { useLocation } from 'react-router-dom';
import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Form, Input, Modal, Typography } from 'antd';
import { useForm } from 'antd/es/form/Form';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { ENTITY_VERSION_V5 } from 'constants/app';
@@ -106,7 +105,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
setCurrentPanelMap(panelMap);
}, [panelMap]);
const [form] = useForm<{
const [form] = Form.useForm<{
title: string;
}>();

View File

@@ -1,4 +1,4 @@
import { MenuItemType } from 'antd/es/menu/hooks/useItems';
import type { MenuItemType } from 'antd/es/menu/hooks/useItems';
import { MenuItemKeys } from './contants';
import { MenuItem } from './types';

View File

@@ -2,7 +2,7 @@ import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { Space, Tooltip } from 'antd';
import { ColumnType } from 'antd/es/table';
import { TableColumnType as ColumnType } from 'antd';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { Events } from 'constants/events';
import { QueryTable } from 'container/QueryTable';

View File

@@ -1,4 +1,4 @@
import { ColumnType } from 'antd/es/table';
import { TableColumnType as ColumnType } from 'antd';
import { convertUnit } from 'container/NewWidget/RightContainer/dataFormatCategories';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { Button, Skeleton, Tag, Typography } from 'antd';
import { useMemo } from 'react';
import { Button } from '@signozhq/button';
import { Skeleton } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetHosts } from 'api/generated/services/zeus';
import ROUTES from 'constants/routes';
@@ -29,77 +30,60 @@ function DataSourceInfo({
query: { enabled: isEnabled || false },
});
const [url, setUrl] = useState<string>('');
const activeHost = useMemo(
() =>
hostsData?.data?.hosts?.find((h) => !h.is_default) ??
hostsData?.data?.hosts?.find((h) => h.is_default),
[hostsData],
);
useEffect(() => {
if (hostsData) {
const defaultHost = hostsData?.data.hosts?.find((h) => h.is_default);
if (defaultHost?.url) {
const url = defaultHost?.url?.split('://')[1] ?? '';
setUrl(url);
}
const url = useMemo(() => activeHost?.url?.split('://')[1] ?? '', [
activeHost,
]);
const handleConnect = (): void => {
logEvent('Homepage: Connect dataSource clicked', {});
if (activeLicense && activeLicense.platform === LicensePlatform.CLOUD) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(DOCS_LINKS.ADD_DATA_SOURCE, '_blank', 'noopener noreferrer');
}
}, [hostsData]);
};
const renderNotSendingData = (): JSX.Element => (
<>
<Typography className="welcome-title">
<h2 className="welcome-title">
Hello there, Welcome to your SigNoz workspace
</Typography>
</h2>
<Typography className="welcome-description">
<p className="welcome-description">
Youre not sending any data yet. <br />
SigNoz is so much better with your data start by sending your telemetry
data to SigNoz.
</Typography>
</p>
<Card className="welcome-card">
<Card.Content>
<div className="workspace-ready-container">
<div className="workspace-ready-header">
<Typography className="workspace-ready-title">
<span className="workspace-ready-title">
<img src="/Icons/hurray.svg" alt="hurray" />
Your workspace is ready
</Typography>
</span>
<Button
type="primary"
variant="solid"
color="primary"
size="sm"
className="periscope-btn primary"
icon={<img src="/Icons/container-plus.svg" alt="plus" />}
prefixIcon={<img src="/Icons/container-plus.svg" alt="plus" />}
onClick={handleConnect}
role="button"
tabIndex={0}
onClick={(): void => {
logEvent('Homepage: Connect dataSource clicked', {});
if (
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
}
}}
onKeyDown={(e): void => {
if (e.key === 'Enter') {
logEvent('Homepage: Connect dataSource clicked', {});
if (
activeLicense &&
activeLicense.platform === LicensePlatform.CLOUD
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
}
handleConnect();
}
}}
>
@@ -112,13 +96,7 @@ function DataSourceInfo({
<div className="workspace-url">
<Link2 size={12} />
<Typography className="workspace-url-text">
{url}
<Tag color="default" className="workspace-url-tag">
default
</Tag>
</Typography>
<span className="workspace-url-text">{url}</span>
</div>
</div>
)}
@@ -130,9 +108,9 @@ function DataSourceInfo({
const renderDataReceived = (): JSX.Element => (
<>
<Typography className="welcome-title">
<h2 className="welcome-title">
Hello there, Welcome to your SigNoz workspace
</Typography>
</h2>
{!isError && hostsData && (
<Card className="welcome-card">
@@ -142,13 +120,7 @@ function DataSourceInfo({
<div className="workspace-url">
<Link2 size={12} />
<Typography className="workspace-url-text">
{url}
<Tag color="default" className="workspace-url-tag">
default
</Tag>
</Typography>
<span className="workspace-url-text">{url}</span>
</div>
</div>
</div>

View File

@@ -30,7 +30,7 @@ const mockHostsResponse: GetHosts200 = {
describe('DataSourceInfo', () => {
afterEach(() => server.resetHandlers());
it('renders the default workspace URL with protocol stripped', async () => {
it('renders the active workspace URL with protocol stripped', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
@@ -39,7 +39,7 @@ describe('DataSourceInfo', () => {
render(<DataSourceInfo dataSentToSigNoz={false} isLoading={false} />);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await screen.findByText(/custom-host\.test\.cloud/i);
});
it('does not render workspace URL when GET /zeus/hosts fails', async () => {
@@ -55,7 +55,7 @@ describe('DataSourceInfo', () => {
expect(screen.queryByText(/signoz\.cloud/i)).not.toBeInTheDocument();
});
it('renders workspace URL in the data-received view when telemetry is flowing', async () => {
it('renders active workspace URL in the data-received view when telemetry is flowing', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
@@ -64,6 +64,6 @@ describe('DataSourceInfo', () => {
render(<DataSourceInfo dataSentToSigNoz={true} isLoading={false} />);
await screen.findByText(/accepted-starfish\.test\.cloud/i);
await screen.findByText(/custom-host\.test\.cloud/i);
});
});

View File

@@ -251,19 +251,11 @@
flex-direction: row;
align-items: center;
gap: 8px;
.workspace-url-tag {
font-size: 10px;
font-weight: 400;
line-height: 18px; /* 150% */
letter-spacing: 0.12px;
border-radius: 3px;
border: 1px solid var(--Slate-400, #1d212d);
background: var(--Ink-400, #121317);
color: var(--Vanilla-400, #c0c1c3);
}
font-size: var(--paragraph-small-400-font-size);
font-weight: var(--paragraph-small-400-font-weight);
line-height: var(--paragraph-small-400-line-height);
letter-spacing: 0.12px;
color: var(--foreground);
}
}

View File

@@ -8,7 +8,7 @@ import {
TableProps,
Typography,
} from 'antd';
import { SorterResult } from 'antd/es/table/interface';
import type { SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { InfraMonitoringEvents } from 'constants/events';

View File

@@ -2,7 +2,7 @@ import { Dispatch, SetStateAction } from 'react';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Progress, TabsProps, Tag, Tooltip, Typography } from 'antd';
import { ColumnType } from 'antd/es/table';
import { TableColumnType as ColumnType } from 'antd';
import {
HostData,
HostListPayload,

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import type { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { K8sClustersData } from 'api/infraMonitoring/getK8sClustersList';
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';

View File

@@ -7,11 +7,12 @@ import {
Button,
Spin,
Table,
TableColumnType as ColumnType,
TablePaginationConfig,
TableProps,
Typography,
} from 'antd';
import { ColumnType, SorterResult } from 'antd/es/table/interface';
import type { SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { K8sClustersListPayload } from 'api/infraMonitoring/getK8sClustersList';
import { InfraMonitoringEvents } from 'constants/events';

View File

@@ -1,6 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { ColumnType } from 'antd/es/table';
import { TableColumnType as ColumnType } from 'antd';
import {
K8sClustersData,
K8sClustersListPayload,

View File

@@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import type { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
import { InfraMonitoringEvents } from 'constants/events';

View File

@@ -7,11 +7,12 @@ import {
Button,
Spin,
Table,
TableColumnType as ColumnType,
TablePaginationConfig,
TableProps,
Typography,
} from 'antd';
import { ColumnType, SorterResult } from 'antd/es/table/interface';
import type { SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { K8sDaemonSetsListPayload } from 'api/infraMonitoring/getK8sDaemonSetsList';
import classNames from 'classnames';

View File

@@ -1,6 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { ColumnType } from 'antd/es/table';
import { TableColumnType as ColumnType } from 'antd';
import {
K8sDaemonSetsData,
K8sDaemonSetsListPayload,

View File

@@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import type { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { K8sDeploymentsData } from 'api/infraMonitoring/getK8sDeploymentsList';
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';

View File

@@ -7,11 +7,12 @@ import {
Button,
Spin,
Table,
TableColumnType as ColumnType,
TablePaginationConfig,
TableProps,
Typography,
} from 'antd';
import { ColumnType, SorterResult } from 'antd/es/table/interface';
import type { SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { K8sDeploymentsListPayload } from 'api/infraMonitoring/getK8sDeploymentsList';
import classNames from 'classnames';

View File

@@ -1,6 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { ColumnType } from 'antd/es/table';
import { TableColumnType as ColumnType } from 'antd';
import {
K8sDeploymentsData,
K8sDeploymentsListPayload,

View File

@@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import type { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';
import { InfraMonitoringEvents } from 'constants/events';

View File

@@ -7,11 +7,12 @@ import {
Button,
Spin,
Table,
TableColumnType as ColumnType,
TablePaginationConfig,
TableProps,
Typography,
} from 'antd';
import { ColumnType, SorterResult } from 'antd/es/table/interface';
import type { SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { K8sJobsListPayload } from 'api/infraMonitoring/getK8sJobsList';
import classNames from 'classnames';

View File

@@ -1,6 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Tag, Tooltip } from 'antd';
import { ColumnType } from 'antd/es/table';
import { TableColumnType as ColumnType } from 'antd';
import {
K8sJobsData,
K8sJobsListPayload,

View File

@@ -7,11 +7,12 @@ import {
Button,
Spin,
Table,
TableColumnType as ColumnType,
TablePaginationConfig,
TableProps,
Typography,
} from 'antd';
import { ColumnType, SorterResult } from 'antd/es/table/interface';
import type { SorterResult } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent';
import { K8sNamespacesListPayload } from 'api/infraMonitoring/getK8sNamespacesList';
import { InfraMonitoringEvents } from 'constants/events';

View File

@@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/lib';
import type { RadioChangeEvent } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import { K8sNamespacesData } from 'api/infraMonitoring/getK8sNamespacesList';
import { VIEW_TYPES, VIEWS } from 'components/HostMetricsDetail/constants';

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