mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 22:35:53 +08:00
feat: support updating subdomain for tenants (#6745)
* feat: support updating subdomain for tenants * feat: enable subdomain update only for cloud accounts * chore: code cleanup
This commit is contained in:
parent
4967696da8
commit
43b0cdbb6a
@ -6,6 +6,7 @@
|
|||||||
"api_keys": "API Keys",
|
"api_keys": "API Keys",
|
||||||
"my_settings": "My Settings",
|
"my_settings": "My Settings",
|
||||||
"overview_metrics": "Overview Metrics",
|
"overview_metrics": "Overview Metrics",
|
||||||
|
"custom_domain_settings": "Custom Domain Settings",
|
||||||
"dbcall_metrics": "Database Calls",
|
"dbcall_metrics": "Database Calls",
|
||||||
"external_metrics": "External Calls",
|
"external_metrics": "External Calls",
|
||||||
"pipeline": "Pipeline",
|
"pipeline": "Pipeline",
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
"ORG_SETTINGS": "SigNoz | Organization Settings",
|
"ORG_SETTINGS": "SigNoz | Organization Settings",
|
||||||
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
|
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
|
||||||
"API_KEYS": "SigNoz | API Keys",
|
"API_KEYS": "SigNoz | API Keys",
|
||||||
|
"CUSTOM_DOMAIN_SETTINGS": "SigNoz | Custom Domain Settings",
|
||||||
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
|
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
|
||||||
"UN_AUTHORIZED": "SigNoz | Unauthorized",
|
"UN_AUTHORIZED": "SigNoz | Unauthorized",
|
||||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"api_keys": "API Keys",
|
"api_keys": "API Keys",
|
||||||
"my_settings": "My Settings",
|
"my_settings": "My Settings",
|
||||||
"overview_metrics": "Overview Metrics",
|
"overview_metrics": "Overview Metrics",
|
||||||
|
"custom_domain_settings": "Custom Domain Settings",
|
||||||
"dbcall_metrics": "Database Calls",
|
"dbcall_metrics": "Database Calls",
|
||||||
"external_metrics": "External Calls",
|
"external_metrics": "External Calls",
|
||||||
"pipeline": "Pipeline",
|
"pipeline": "Pipeline",
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
"ORG_SETTINGS": "SigNoz | Organization Settings",
|
"ORG_SETTINGS": "SigNoz | Organization Settings",
|
||||||
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
|
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
|
||||||
"API_KEYS": "SigNoz | API Keys",
|
"API_KEYS": "SigNoz | API Keys",
|
||||||
|
"CUSTOM_DOMAIN_SETTINGS": "SigNoz | Custom Domain Settings",
|
||||||
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
|
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
|
||||||
"UN_AUTHORIZED": "SigNoz | Unauthorized",
|
"UN_AUTHORIZED": "SigNoz | Unauthorized",
|
||||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||||
|
@ -145,6 +145,11 @@ export const MySettings = Loadable(
|
|||||||
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
|
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const CustomDomainSettings = Loadable(
|
||||||
|
() =>
|
||||||
|
import(/* webpackChunkName: "Custom Domain Settings" */ 'pages/Settings'),
|
||||||
|
);
|
||||||
|
|
||||||
export const Logs = Loadable(
|
export const Logs = Loadable(
|
||||||
() => import(/* webpackChunkName: "Logs" */ 'pages/LogsModulePage'),
|
() => import(/* webpackChunkName: "Logs" */ 'pages/LogsModulePage'),
|
||||||
);
|
);
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
BillingPage,
|
BillingPage,
|
||||||
CreateAlertChannelAlerts,
|
CreateAlertChannelAlerts,
|
||||||
CreateNewAlerts,
|
CreateNewAlerts,
|
||||||
|
CustomDomainSettings,
|
||||||
DashboardPage,
|
DashboardPage,
|
||||||
DashboardWidget,
|
DashboardWidget,
|
||||||
EditAlertChannelsAlerts,
|
EditAlertChannelsAlerts,
|
||||||
@ -288,6 +289,13 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'MY_SETTINGS',
|
key: 'MY_SETTINGS',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.CUSTOM_DOMAIN_SETTINGS,
|
||||||
|
exact: true,
|
||||||
|
component: CustomDomainSettings,
|
||||||
|
isPrivate: true,
|
||||||
|
key: 'CUSTOM_DOMAIN_SETTINGS',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.LOGS,
|
path: ROUTES.LOGS,
|
||||||
exact: true,
|
exact: true,
|
||||||
|
7
frontend/src/api/customDomain/getDeploymentsData.ts
Normal file
7
frontend/src/api/customDomain/getDeploymentsData.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { GatewayApiV2Instance as axios } from 'api';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { DeploymentsDataProps } from 'types/api/customDomain/types';
|
||||||
|
|
||||||
|
export const getDeploymentsData = (): Promise<
|
||||||
|
AxiosResponse<DeploymentsDataProps>
|
||||||
|
> => axios.get(`/deployments/me`);
|
16
frontend/src/api/customDomain/updateSubDomain.ts
Normal file
16
frontend/src/api/customDomain/updateSubDomain.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { GatewayApiV2Instance as axios } from 'api';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
PayloadProps,
|
||||||
|
UpdateCustomDomainProps,
|
||||||
|
} from 'types/api/customDomain/types';
|
||||||
|
|
||||||
|
const updateSubDomainAPI = async (
|
||||||
|
props: UpdateCustomDomainProps,
|
||||||
|
): Promise<SuccessResponse<PayloadProps> | AxiosError> =>
|
||||||
|
axios.put(`/deployments/me/host`, {
|
||||||
|
...props.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default updateSubDomainAPI;
|
@ -127,7 +127,6 @@ function TimezonePicker({
|
|||||||
setIsOpen,
|
setIsOpen,
|
||||||
isOpenedFromFooter,
|
isOpenedFromFooter,
|
||||||
}: TimezonePickerProps): JSX.Element {
|
}: TimezonePickerProps): JSX.Element {
|
||||||
console.log({ isOpenedFromFooter });
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const { timezone, updateTimezone } = useTimezone();
|
const { timezone, updateTimezone } = useTimezone();
|
||||||
const [selectedTimezone, setSelectedTimezone] = useState<string>(
|
const [selectedTimezone, setSelectedTimezone] = useState<string>(
|
||||||
|
@ -34,6 +34,7 @@ const ROUTES = {
|
|||||||
MY_SETTINGS: '/my-settings',
|
MY_SETTINGS: '/my-settings',
|
||||||
SETTINGS: '/settings',
|
SETTINGS: '/settings',
|
||||||
ORG_SETTINGS: '/settings/org-settings',
|
ORG_SETTINGS: '/settings/org-settings',
|
||||||
|
CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings',
|
||||||
API_KEYS: '/settings/api-keys',
|
API_KEYS: '/settings/api-keys',
|
||||||
INGESTION_SETTINGS: '/settings/ingestion-settings',
|
INGESTION_SETTINGS: '/settings/ingestion-settings',
|
||||||
SOMETHING_WENT_WRONG: '/something-went-wrong',
|
SOMETHING_WENT_WRONG: '/something-went-wrong',
|
||||||
|
@ -0,0 +1,262 @@
|
|||||||
|
.custom-domain-settings-container {
|
||||||
|
margin-top: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.custom-domain-settings-content {
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
max-width: 736px;
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-settings-card {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,300 @@
|
|||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
import './CustomDomainSettings.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Skeleton,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import updateSubDomainAPI from 'api/customDomain/updateSubDomain';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
||||||
|
import { useGetDeploymentsData } from 'hooks/CustomDomain/useGetDeploymentsData';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { InfoIcon, Link2, Pencil } from 'lucide-react';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useMutation } from 'react-query';
|
||||||
|
import { useCopyToClipboard } from 'react-use';
|
||||||
|
import { HostsProps } from 'types/api/customDomain/types';
|
||||||
|
|
||||||
|
interface CustomDomainSettingsProps {
|
||||||
|
subdomain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CustomDomainSettings(): JSX.Element {
|
||||||
|
const { org } = useAppContext();
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
const [isPollingEnabled, setIsPollingEnabled] = useState(false);
|
||||||
|
const [hosts, setHosts] = useState<HostsProps[] | null>(null);
|
||||||
|
|
||||||
|
const [updateDomainError, setUpdateDomainError] = useState<AxiosError | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [, setCopyUrl] = useCopyToClipboard();
|
||||||
|
|
||||||
|
const [
|
||||||
|
customDomainDetails,
|
||||||
|
setCustomDomainDetails,
|
||||||
|
] = useState<CustomDomainSettingsProps | null>();
|
||||||
|
|
||||||
|
const [editForm] = Form.useForm();
|
||||||
|
|
||||||
|
const handleModalClose = (): void => {
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
editForm.resetFields();
|
||||||
|
setUpdateDomainError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: deploymentsData,
|
||||||
|
isLoading: isLoadingDeploymentsData,
|
||||||
|
isFetching: isFetchingDeploymentsData,
|
||||||
|
refetch: refetchDeploymentsData,
|
||||||
|
} = useGetDeploymentsData();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: updateSubDomain,
|
||||||
|
isLoading: isLoadingUpdateCustomDomain,
|
||||||
|
} = useMutation(updateSubDomainAPI, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsPollingEnabled(true);
|
||||||
|
refetchDeploymentsData();
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError) => {
|
||||||
|
setUpdateDomainError(error);
|
||||||
|
setIsPollingEnabled(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (deploymentsData?.data?.status === 'success') {
|
||||||
|
setHosts(deploymentsData.data.data.hosts);
|
||||||
|
|
||||||
|
const activeCustomDomain = deploymentsData.data.data.hosts.find(
|
||||||
|
(host) => !host.is_default,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeCustomDomain) {
|
||||||
|
setCustomDomainDetails({
|
||||||
|
subdomain: activeCustomDomain?.name || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deploymentsData?.data?.data?.state !== 'HEALTHY' && isPollingEnabled) {
|
||||||
|
setTimeout(() => {
|
||||||
|
refetchDeploymentsData();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deploymentsData?.data?.data.state === 'HEALTHY') {
|
||||||
|
setIsPollingEnabled(false);
|
||||||
|
}
|
||||||
|
}, [deploymentsData, refetchDeploymentsData, isPollingEnabled]);
|
||||||
|
|
||||||
|
const onUpdateCustomDomainSettings = (): void => {
|
||||||
|
editForm
|
||||||
|
.validateFields()
|
||||||
|
.then((values) => {
|
||||||
|
if (values.subdomain) {
|
||||||
|
updateSubDomain({
|
||||||
|
data: {
|
||||||
|
name: values.subdomain,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setCustomDomainDetails({
|
||||||
|
subdomain: values.subdomain,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((errorInfo) => {
|
||||||
|
console.error('error info', errorInfo);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCopyUrlHandler = (host: string): void => {
|
||||||
|
const url = `${host}.${deploymentsData?.data.data.cluster.region.dns}`;
|
||||||
|
|
||||||
|
setCopyUrl(url);
|
||||||
|
notifications.success({
|
||||||
|
message: 'Copied to clipboard',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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">
|
||||||
|
{!isLoadingDeploymentsData && (
|
||||||
|
<Card className="custom-domain-settings-card">
|
||||||
|
<div className="custom-domain-settings-content-header">
|
||||||
|
Team {org?.[0]?.name} Information
|
||||||
|
</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.name)}
|
||||||
|
>
|
||||||
|
<Link2 size={12} /> {host.name}.
|
||||||
|
{deploymentsData?.data.data.cluster.region.dns}
|
||||||
|
{host.is_default && <Tag color={Color.BG_ROBIN_500}>Default</Tag>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="custom-domain-url-edit-btn">
|
||||||
|
<Button
|
||||||
|
className="periscope-btn"
|
||||||
|
disabled={
|
||||||
|
isLoadingDeploymentsData ||
|
||||||
|
isFetchingDeploymentsData ||
|
||||||
|
isPollingEnabled
|
||||||
|
}
|
||||||
|
type="default"
|
||||||
|
icon={<Pencil size={10} />}
|
||||||
|
onClick={(): void => setIsEditModalOpen(true)}
|
||||||
|
>
|
||||||
|
Customize team’s URL
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isPollingEnabled && (
|
||||||
|
<Alert
|
||||||
|
className="custom-domain-update-status"
|
||||||
|
message={`Updating your URL to ⎯ ${customDomainDetails?.subdomain}.${deploymentsData?.data.data.cluster.region.dns}. This may take a few mins.`}
|
||||||
|
type="info"
|
||||||
|
icon={<InfoIcon size={12} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoadingDeploymentsData && (
|
||||||
|
<Card className="custom-domain-settings-card">
|
||||||
|
<Skeleton
|
||||||
|
className="custom-domain-settings-skeleton"
|
||||||
|
active
|
||||||
|
paragraph={{ rows: 2 }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Update Custom Domain Modal */}
|
||||||
|
<Modal
|
||||||
|
className="custom-domain-settings-modal"
|
||||||
|
title="Customize your team’s 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="Team’s 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={deploymentsData?.data.data.cluster.region.dns}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{updateDomainError && (
|
||||||
|
<div className="custom-domain-settings-modal-error">
|
||||||
|
{updateDomainError.status === 409 ? (
|
||||||
|
<Alert
|
||||||
|
message="You’ve 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 { error: string })?.error}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
3
frontend/src/container/CustomDomainSettings/index.tsx
Normal file
3
frontend/src/container/CustomDomainSettings/index.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import CustomDomainSettings from './CustomDomainSettings';
|
||||||
|
|
||||||
|
export default CustomDomainSettings;
|
@ -253,7 +253,7 @@ export function formatWithTimezone(
|
|||||||
): string {
|
): string {
|
||||||
const parsedDate =
|
const parsedDate =
|
||||||
typeof dateValue === 'string' ? dateValue : dateValue?.format();
|
typeof dateValue === 'string' ? dateValue : dateValue?.format();
|
||||||
console.log('dateValue', parsedDate, 'timezone', timezone);
|
|
||||||
// Get the target timezone offset
|
// Get the target timezone offset
|
||||||
const targetOffset = convertUtcOffsetToTimezoneOffset(
|
const targetOffset = convertUtcOffsetToTimezoneOffset(
|
||||||
dayjs(dateValue).tz(timezone).utcOffset(),
|
dayjs(dateValue).tz(timezone).utcOffset(),
|
||||||
|
@ -17,6 +17,7 @@ const breadcrumbNameMap: Record<string, string> = {
|
|||||||
[ROUTES.ORG_SETTINGS]: 'Organization Settings',
|
[ROUTES.ORG_SETTINGS]: 'Organization Settings',
|
||||||
[ROUTES.INGESTION_SETTINGS]: 'Ingestion Settings',
|
[ROUTES.INGESTION_SETTINGS]: 'Ingestion Settings',
|
||||||
[ROUTES.MY_SETTINGS]: 'My Settings',
|
[ROUTES.MY_SETTINGS]: 'My Settings',
|
||||||
|
[ROUTES.CUSTOM_DOMAIN_SETTINGS]: 'Custom Domain Settings',
|
||||||
[ROUTES.ERROR_DETAIL]: 'Exceptions',
|
[ROUTES.ERROR_DETAIL]: 'Exceptions',
|
||||||
[ROUTES.LIST_ALL_ALERT]: 'Alerts',
|
[ROUTES.LIST_ALL_ALERT]: 'Alerts',
|
||||||
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
|
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
|
||||||
|
@ -190,6 +190,7 @@ export const routesToSkip = [
|
|||||||
ROUTES.ALL_DASHBOARD,
|
ROUTES.ALL_DASHBOARD,
|
||||||
ROUTES.ORG_SETTINGS,
|
ROUTES.ORG_SETTINGS,
|
||||||
ROUTES.INGESTION_SETTINGS,
|
ROUTES.INGESTION_SETTINGS,
|
||||||
|
ROUTES.CUSTOM_DOMAIN_SETTINGS,
|
||||||
ROUTES.API_KEYS,
|
ROUTES.API_KEYS,
|
||||||
ROUTES.ERROR_DETAIL,
|
ROUTES.ERROR_DETAIL,
|
||||||
ROUTES.LOGS_PIPELINES,
|
ROUTES.LOGS_PIPELINES,
|
||||||
|
13
frontend/src/hooks/CustomDomain/useGetDeploymentsData.ts
Normal file
13
frontend/src/hooks/CustomDomain/useGetDeploymentsData.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { getDeploymentsData } from 'api/customDomain/getDeploymentsData';
|
||||||
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
|
import { useQuery, UseQueryResult } from 'react-query';
|
||||||
|
import { DeploymentsDataProps } from 'types/api/customDomain/types';
|
||||||
|
|
||||||
|
export const useGetDeploymentsData = (): UseQueryResult<
|
||||||
|
AxiosResponse<DeploymentsDataProps>,
|
||||||
|
AxiosError
|
||||||
|
> =>
|
||||||
|
useQuery<AxiosResponse<DeploymentsDataProps>, AxiosError>({
|
||||||
|
queryKey: ['getDeploymentsData'],
|
||||||
|
queryFn: () => getDeploymentsData(),
|
||||||
|
});
|
@ -2,13 +2,21 @@ import { RouteTabProps } from 'components/RouteTab/types';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import AlertChannels from 'container/AllAlertChannels';
|
import AlertChannels from 'container/AllAlertChannels';
|
||||||
import APIKeys from 'container/APIKeys/APIKeys';
|
import APIKeys from 'container/APIKeys/APIKeys';
|
||||||
|
import CustomDomainSettings from 'container/CustomDomainSettings';
|
||||||
import GeneralSettings from 'container/GeneralSettings';
|
import GeneralSettings from 'container/GeneralSettings';
|
||||||
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
||||||
import IngestionSettings from 'container/IngestionSettings/IngestionSettings';
|
import IngestionSettings from 'container/IngestionSettings/IngestionSettings';
|
||||||
import MultiIngestionSettings from 'container/IngestionSettings/MultiIngestionSettings';
|
import MultiIngestionSettings from 'container/IngestionSettings/MultiIngestionSettings';
|
||||||
import OrganizationSettings from 'container/OrganizationSettings';
|
import OrganizationSettings from 'container/OrganizationSettings';
|
||||||
import { TFunction } from 'i18next';
|
import { TFunction } from 'i18next';
|
||||||
import { Backpack, BellDot, Building, Cpu, KeySquare } from 'lucide-react';
|
import {
|
||||||
|
Backpack,
|
||||||
|
BellDot,
|
||||||
|
Building,
|
||||||
|
Cpu,
|
||||||
|
Globe,
|
||||||
|
KeySquare,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [
|
export const organizationSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||||
{
|
{
|
||||||
@ -102,3 +110,16 @@ export const apiKeys = (t: TFunction): RouteTabProps['routes'] => [
|
|||||||
key: ROUTES.API_KEYS,
|
key: ROUTES.API_KEYS,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const customDomainSettings = (t: TFunction): RouteTabProps['routes'] => [
|
||||||
|
{
|
||||||
|
Component: CustomDomainSettings,
|
||||||
|
name: (
|
||||||
|
<div className="periscope-tab">
|
||||||
|
<Globe size={16} /> {t('routes:custom_domain_settings').toString()}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
route: ROUTES.CUSTOM_DOMAIN_SETTINGS,
|
||||||
|
key: ROUTES.CUSTOM_DOMAIN_SETTINGS,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@ -6,6 +6,7 @@ import { isCloudUser, isEECloudUser } from 'utils/app';
|
|||||||
import {
|
import {
|
||||||
alertChannels,
|
alertChannels,
|
||||||
apiKeys,
|
apiKeys,
|
||||||
|
customDomainSettings,
|
||||||
generalSettings,
|
generalSettings,
|
||||||
ingestionSettings,
|
ingestionSettings,
|
||||||
multiIngestionSettings,
|
multiIngestionSettings,
|
||||||
@ -20,28 +21,35 @@ export const getRoutes = (
|
|||||||
): RouteTabProps['routes'] => {
|
): RouteTabProps['routes'] => {
|
||||||
const settings = [];
|
const settings = [];
|
||||||
|
|
||||||
|
const isCloudAccount = isCloudUser();
|
||||||
|
const isEECloudAccount = isEECloudUser();
|
||||||
|
|
||||||
|
const isAdmin = userRole === USER_ROLES.ADMIN;
|
||||||
|
const isEditor = userRole === USER_ROLES.EDITOR;
|
||||||
|
|
||||||
settings.push(...generalSettings(t));
|
settings.push(...generalSettings(t));
|
||||||
|
|
||||||
if (isCurrentOrgSettings) {
|
if (isCurrentOrgSettings) {
|
||||||
settings.push(...organizationSettings(t));
|
settings.push(...organizationSettings(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (isGatewayEnabled && (isAdmin || isEditor)) {
|
||||||
isGatewayEnabled &&
|
|
||||||
(userRole === USER_ROLES.ADMIN || userRole === USER_ROLES.EDITOR)
|
|
||||||
) {
|
|
||||||
settings.push(...multiIngestionSettings(t));
|
settings.push(...multiIngestionSettings(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCloudUser() && !isGatewayEnabled) {
|
if (isCloudAccount && !isGatewayEnabled) {
|
||||||
settings.push(...ingestionSettings(t));
|
settings.push(...ingestionSettings(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.push(...alertChannels(t));
|
settings.push(...alertChannels(t));
|
||||||
|
|
||||||
if ((isCloudUser() || isEECloudUser()) && userRole === USER_ROLES.ADMIN) {
|
if ((isCloudAccount || isEECloudAccount) && isAdmin) {
|
||||||
settings.push(...apiKeys(t));
|
settings.push(...apiKeys(t));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCloudAccount && isAdmin) {
|
||||||
|
settings.push(...customDomainSettings(t));
|
||||||
|
}
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
};
|
};
|
||||||
|
53
frontend/src/types/api/customDomain/types.ts
Normal file
53
frontend/src/types/api/customDomain/types.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
export interface HostsProps {
|
||||||
|
name: string;
|
||||||
|
is_default: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegionProps {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
dns: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClusterProps {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
cloud_account_id: string;
|
||||||
|
cloud_region: string;
|
||||||
|
address: string;
|
||||||
|
region: RegionProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeploymentData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
state: string;
|
||||||
|
tier: string;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
cluster_id: string;
|
||||||
|
hosts: HostsProps[];
|
||||||
|
cluster: ClusterProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeploymentsDataProps {
|
||||||
|
status: string;
|
||||||
|
data: DeploymentData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PayloadProps = {
|
||||||
|
status: string;
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UpdateCustomDomainProps {
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
@ -100,6 +100,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
|||||||
LOGS_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
LOGS_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
TRACES_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
TRACES_SAVE_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
API_KEYS: ['ADMIN'],
|
API_KEYS: ['ADMIN'],
|
||||||
|
CUSTOM_DOMAIN_SETTINGS: ['ADMIN'],
|
||||||
LOGS_BASE: [],
|
LOGS_BASE: [],
|
||||||
OLD_LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
OLD_LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user