mirror of
https://github.com/logto-io/logto.git
synced 2025-03-03 22:15:32 -05:00
feat(console): add console UI for private keys rotation (#4632)
This commit is contained in:
parent
5db08a7a20
commit
106ec388d5
10 changed files with 312 additions and 18 deletions
|
@ -28,4 +28,5 @@
|
||||||
background: transparent;
|
background: transparent;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
z-index: 102;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { useRef } from 'react';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
|
|
||||||
import usePosition from '@/hooks/use-position';
|
import usePosition from '@/hooks/use-position';
|
||||||
import type { HorizontalAlignment } from '@/types/positioning';
|
import type { HorizontalAlignment, VerticalAlignment } from '@/types/positioning';
|
||||||
import { onKeyDownHandler } from '@/utils/a11y';
|
import { onKeyDownHandler } from '@/utils/a11y';
|
||||||
|
|
||||||
import OverlayScrollbar from '../OverlayScrollbar';
|
import OverlayScrollbar from '../OverlayScrollbar';
|
||||||
|
@ -29,6 +29,7 @@ type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
titleClassName?: string;
|
titleClassName?: string;
|
||||||
horizontalAlign?: HorizontalAlignment;
|
horizontalAlign?: HorizontalAlignment;
|
||||||
|
verticalAlign?: VerticalAlignment;
|
||||||
hasOverflowContent?: boolean;
|
hasOverflowContent?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -49,14 +50,15 @@ function Dropdown({
|
||||||
className,
|
className,
|
||||||
titleClassName,
|
titleClassName,
|
||||||
horizontalAlign = 'end',
|
horizontalAlign = 'end',
|
||||||
|
verticalAlign = 'bottom',
|
||||||
hasOverflowContent,
|
hasOverflowContent,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const overlayRef = useRef<HTMLDivElement>(null);
|
const overlayRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { position, positionState, mutate } = usePosition({
|
const { position, positionState, mutate } = usePosition({
|
||||||
verticalAlign: 'bottom',
|
verticalAlign,
|
||||||
horizontalAlign,
|
horizontalAlign,
|
||||||
offset: { vertical: 4, horizontal: 0 },
|
offset: { vertical: verticalAlign === 'bottom' ? 4 : -4, horizontal: 0 },
|
||||||
anchorRef,
|
anchorRef,
|
||||||
overlayRef,
|
overlayRef,
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useRef, useState } from 'react';
|
||||||
import Close from '@/assets/icons/close.svg';
|
import Close from '@/assets/icons/close.svg';
|
||||||
import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg';
|
import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg';
|
||||||
import KeyboardArrowUp from '@/assets/icons/keyboard-arrow-up.svg';
|
import KeyboardArrowUp from '@/assets/icons/keyboard-arrow-up.svg';
|
||||||
|
import { type VerticalAlignment } from '@/types/positioning';
|
||||||
import { onKeyDownHandler } from '@/utils/a11y';
|
import { onKeyDownHandler } from '@/utils/a11y';
|
||||||
|
|
||||||
import Dropdown, { DropdownItem } from '../Dropdown';
|
import Dropdown, { DropdownItem } from '../Dropdown';
|
||||||
|
@ -27,6 +28,7 @@ type Props<T> = {
|
||||||
placeholder?: ReactNode;
|
placeholder?: ReactNode;
|
||||||
isClearable?: boolean;
|
isClearable?: boolean;
|
||||||
size?: 'small' | 'medium' | 'large';
|
size?: 'small' | 'medium' | 'large';
|
||||||
|
dropdownPosition?: VerticalAlignment;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Select<T extends string>({
|
function Select<T extends string>({
|
||||||
|
@ -39,6 +41,7 @@ function Select<T extends string>({
|
||||||
placeholder,
|
placeholder,
|
||||||
isClearable,
|
isClearable,
|
||||||
size = 'large',
|
size = 'large',
|
||||||
|
dropdownPosition = 'bottom',
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const anchorRef = useRef<HTMLInputElement>(null);
|
const anchorRef = useRef<HTMLInputElement>(null);
|
||||||
|
@ -97,6 +100,7 @@ function Select<T extends string>({
|
||||||
</div>
|
</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
isFullWidth
|
isFullWidth
|
||||||
|
verticalAlign={dropdownPosition}
|
||||||
anchorRef={anchorRef}
|
anchorRef={anchorRef}
|
||||||
className={styles.dropdown}
|
className={styles.dropdown}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
|
|
@ -124,6 +124,14 @@
|
||||||
.headerTable {
|
.headerTable {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 1px solid var(--color-divider);
|
border: 1px solid var(--color-divider);
|
||||||
|
|
||||||
|
tr th:first-child {
|
||||||
|
padding-left: _.unit(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr th:last-child {
|
||||||
|
padding-right: _.unit(7);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bodyTable {
|
.bodyTable {
|
||||||
|
@ -134,6 +142,14 @@
|
||||||
tr:first-child td {
|
tr:first-child td {
|
||||||
border-top: 1px solid transparent;
|
border-top: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tr td:first-child {
|
||||||
|
padding-left: _.unit(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr td:last-child {
|
||||||
|
padding-right: _.unit(7);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.hoverEffect:hover {
|
tr.hoverEffect:hover {
|
||||||
|
|
|
@ -33,6 +33,7 @@ export type Props<
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
pagination?: PaginationProps;
|
pagination?: PaginationProps;
|
||||||
placeholder?: ReactNode;
|
placeholder?: ReactNode;
|
||||||
|
loadingSkeleton?: ReactNode;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
hasBorder?: boolean;
|
hasBorder?: boolean;
|
||||||
onRetry?: () => void;
|
onRetry?: () => void;
|
||||||
|
@ -56,6 +57,7 @@ function Table<
|
||||||
isLoading,
|
isLoading,
|
||||||
pagination,
|
pagination,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
loadingSkeleton,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
hasBorder,
|
hasBorder,
|
||||||
onRetry,
|
onRetry,
|
||||||
|
@ -103,9 +105,10 @@ function Table<
|
||||||
>
|
>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{isLoading && (
|
{isLoading &&
|
||||||
<TableLoading columnSpans={columns.map(({ colSpan }) => colSpan ?? 1)} />
|
(loadingSkeleton ?? (
|
||||||
)}
|
<TableLoading columnSpans={columns.map(({ colSpan }) => colSpan ?? 1)} />
|
||||||
|
))}
|
||||||
{hasError && (
|
{hasError && (
|
||||||
<TableError columns={columns.length} content={errorMessage} onRetry={onRetry} />
|
<TableError columns={columns.length} content={errorMessage} onRetry={onRetry} />
|
||||||
)}
|
)}
|
||||||
|
@ -145,7 +148,7 @@ function Table<
|
||||||
>
|
>
|
||||||
{columns.map(({ dataIndex, colSpan, className, render }) => (
|
{columns.map(({ dataIndex, colSpan, className, render }) => (
|
||||||
<td key={dataIndex} colSpan={colSpan} className={className}>
|
<td key={dataIndex} colSpan={colSpan} className={className}>
|
||||||
{render(row)}
|
{render(row, rowIndex)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { FieldValues } from 'react-hook-form';
|
||||||
export type Column<TFieldValues extends FieldValues = FieldValues> = {
|
export type Column<TFieldValues extends FieldValues = FieldValues> = {
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
dataIndex: string;
|
dataIndex: string;
|
||||||
render: (row: TFieldValues) => ReactNode;
|
render: (row: TFieldValues, rowIndex: number) => ReactNode;
|
||||||
colSpan?: number;
|
colSpan?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.idWrapper {
|
||||||
|
font: var(--font-body-2);
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 26px;
|
||||||
|
padding: 0 _.unit(2);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--color-bg-info-tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteIcon {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotateKey {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--color-divider);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: _.unit(4);
|
||||||
|
gap: _.unit(2);
|
||||||
|
|
||||||
|
.description {
|
||||||
|
flex: 1;
|
||||||
|
font: var(--font-body-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bone {
|
||||||
|
@include _.shimmering-animation;
|
||||||
|
height: 26px;
|
||||||
|
max-width: 344px;
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
import {
|
||||||
|
LogtoOidcConfigKey,
|
||||||
|
SupportedSigningKeyAlgorithm,
|
||||||
|
type OidcConfigKeysResponse,
|
||||||
|
} from '@logto/schemas';
|
||||||
|
import { condArray } from '@silverhand/essentials';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import Delete from '@/assets/icons/delete.svg';
|
||||||
|
import FormCard from '@/components/FormCard';
|
||||||
|
import Button from '@/ds-components/Button';
|
||||||
|
import DangerConfirmModal from '@/ds-components/DeleteConfirmModal';
|
||||||
|
import DynamicT from '@/ds-components/DynamicT';
|
||||||
|
import FormField from '@/ds-components/FormField';
|
||||||
|
import IconButton from '@/ds-components/IconButton';
|
||||||
|
import Select from '@/ds-components/Select';
|
||||||
|
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
|
||||||
|
import Table from '@/ds-components/Table';
|
||||||
|
import Tag from '@/ds-components/Tag';
|
||||||
|
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
function SigningKeys() {
|
||||||
|
const api = useApi();
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.tenants.signing_keys' });
|
||||||
|
const [activeTab, setActiveTab] = useState<LogtoOidcConfigKey>(LogtoOidcConfigKey.PrivateKeys);
|
||||||
|
const keyType = activeTab === LogtoOidcConfigKey.PrivateKeys ? 'private-keys' : 'cookie-keys';
|
||||||
|
const entities = activeTab === LogtoOidcConfigKey.PrivateKeys ? 'tokens' : 'cookies';
|
||||||
|
|
||||||
|
const { data, error, mutate } = useSWR<OidcConfigKeysResponse[], RequestError>(
|
||||||
|
`api/configs/oidc/${keyType}`
|
||||||
|
);
|
||||||
|
const [deletingKeyId, setDeletingKeyId] = useState<string>();
|
||||||
|
const [showRotateConfirmModal, setShowRotateConfirmModal] = useState(false);
|
||||||
|
const [isRotating, setIsRotating] = useState(false);
|
||||||
|
const [rotateKeyAlgorithm, setRotateKeyAlgorithm] = useState<SupportedSigningKeyAlgorithm>(
|
||||||
|
SupportedSigningKeyAlgorithm.EC
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLoadingKeys = !data && !error;
|
||||||
|
|
||||||
|
const tableColumns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
title: t('table_column.id'),
|
||||||
|
dataIndex: 'id',
|
||||||
|
colSpan: 8,
|
||||||
|
render: ({ id }: OidcConfigKeysResponse) => <span className={styles.idWrapper}>{id}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('table_column.status'),
|
||||||
|
dataIndex: 'status',
|
||||||
|
colSpan: 4,
|
||||||
|
render: (_: OidcConfigKeysResponse, rowIndex: number) => (
|
||||||
|
<Tag type="state" status={rowIndex === 0 ? 'success' : 'alert'}>
|
||||||
|
{t(rowIndex === 0 ? 'status.current' : 'status.previous')}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
...condArray(
|
||||||
|
activeTab === LogtoOidcConfigKey.PrivateKeys && [
|
||||||
|
{
|
||||||
|
title: t('table_column.algorithm'),
|
||||||
|
dataIndex: 'signingKeyAlgorithm',
|
||||||
|
colSpan: 7,
|
||||||
|
render: ({ signingKeyAlgorithm }: OidcConfigKeysResponse) => (
|
||||||
|
<span>{signingKeyAlgorithm}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
),
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
dataIndex: 'action',
|
||||||
|
colSpan: 2,
|
||||||
|
render: ({ id }, rowIndex) =>
|
||||||
|
rowIndex !== 0 && (
|
||||||
|
<div className={styles.deleteIcon}>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
setDeletingKeyId(id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[activeTab, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormCard title="tenants.signing_keys.title" description="tenants.signing_keys.description">
|
||||||
|
<TabNav>
|
||||||
|
<TabNavItem
|
||||||
|
isActive={activeTab === LogtoOidcConfigKey.PrivateKeys}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTab(LogtoOidcConfigKey.PrivateKeys);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DynamicT forKey="tenants.signing_keys.type.private_key" />
|
||||||
|
</TabNavItem>
|
||||||
|
<TabNavItem
|
||||||
|
isActive={activeTab === LogtoOidcConfigKey.CookieKeys}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTab(LogtoOidcConfigKey.CookieKeys);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DynamicT forKey="tenants.signing_keys.type.cookie_key" />
|
||||||
|
</TabNavItem>
|
||||||
|
</TabNav>
|
||||||
|
<FormField title="tenants.signing_keys.private_keys_in_use">
|
||||||
|
<Table
|
||||||
|
hasBorder
|
||||||
|
isLoading={isLoadingKeys || isRotating}
|
||||||
|
errorMessage={error?.body?.message ?? error?.message}
|
||||||
|
rowIndexKey="id"
|
||||||
|
rowGroups={[{ key: 'signing_keys', data }]}
|
||||||
|
columns={tableColumns}
|
||||||
|
loadingSkeleton={
|
||||||
|
<>
|
||||||
|
{Array.from({ length: 2 }).map((_, rowIndex) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<tr key={`skeleton-row-${rowIndex}`}>
|
||||||
|
{tableColumns.map(({ colSpan }, columnIndex) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<td key={columnIndex} colSpan={colSpan}>
|
||||||
|
<div className={styles.bone} />
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField title="tenants.signing_keys.rotate_private_keys">
|
||||||
|
<div className={styles.rotateKey}>
|
||||||
|
<div className={styles.description}>
|
||||||
|
{t('rotate_private_keys_description', { entities })}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
title="tenants.signing_keys.rotate_private_keys"
|
||||||
|
type="default"
|
||||||
|
onClick={() => {
|
||||||
|
setShowRotateConfirmModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormField>
|
||||||
|
<DangerConfirmModal
|
||||||
|
confirmButtonText="tenants.signing_keys.rotate_button"
|
||||||
|
isOpen={showRotateConfirmModal}
|
||||||
|
onCancel={() => {
|
||||||
|
setShowRotateConfirmModal(false);
|
||||||
|
}}
|
||||||
|
onConfirm={async () => {
|
||||||
|
setIsRotating(true);
|
||||||
|
setShowRotateConfirmModal(false);
|
||||||
|
try {
|
||||||
|
const keys = await api
|
||||||
|
.post(`api/configs/oidc/${keyType}/rotate`, {
|
||||||
|
json: { signingKeyAlgorithm: rotateKeyAlgorithm },
|
||||||
|
})
|
||||||
|
.json<OidcConfigKeysResponse[]>();
|
||||||
|
void mutate(keys);
|
||||||
|
} finally {
|
||||||
|
setIsRotating(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Trans components={{ strong: <strong /> }}>
|
||||||
|
{t('reminder.rotate', { key: activeTab, entities })}
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
{activeTab === LogtoOidcConfigKey.PrivateKeys && (
|
||||||
|
<FormField title="tenants.signing_keys.select_private_key_algorithm">
|
||||||
|
<Select
|
||||||
|
options={Object.values(SupportedSigningKeyAlgorithm).map((value) => ({
|
||||||
|
title: value,
|
||||||
|
value,
|
||||||
|
}))}
|
||||||
|
dropdownPosition="top"
|
||||||
|
value={rotateKeyAlgorithm}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRotateKeyAlgorithm(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
</DangerConfirmModal>
|
||||||
|
<DangerConfirmModal
|
||||||
|
isOpen={Boolean(deletingKeyId)}
|
||||||
|
onCancel={() => {
|
||||||
|
setDeletingKeyId(undefined);
|
||||||
|
}}
|
||||||
|
onConfirm={async () => {
|
||||||
|
if (!deletingKeyId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api.delete(`api/configs/oidc/${keyType}/${deletingKeyId}`);
|
||||||
|
void mutate(data?.filter((key) => key.id !== deletingKeyId));
|
||||||
|
} finally {
|
||||||
|
setDeletingKeyId(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Trans components={{ strong: <strong /> }}>
|
||||||
|
{t('reminder.delete', { key: activeTab, entities })}
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
</DangerConfirmModal>
|
||||||
|
</FormCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SigningKeys;
|
|
@ -16,6 +16,7 @@ import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||||
import DeleteCard from './DeleteCard';
|
import DeleteCard from './DeleteCard';
|
||||||
import DeleteModal from './DeleteModal';
|
import DeleteModal from './DeleteModal';
|
||||||
import ProfileForm from './ProfileForm';
|
import ProfileForm from './ProfileForm';
|
||||||
|
import SigningKeys from './SigningKeys';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
import { type TenantSettingsForm } from './types.js';
|
import { type TenantSettingsForm } from './types.js';
|
||||||
|
|
||||||
|
@ -114,6 +115,7 @@ function TenantBasicSettings() {
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<div className={styles.fields}>
|
<div className={styles.fields}>
|
||||||
<ProfileForm currentTenantId={currentTenantId} />
|
<ProfileForm currentTenantId={currentTenantId} />
|
||||||
|
<SigningKeys />
|
||||||
<DeleteCard currentTenantId={currentTenantId} onClick={onClickDeletionButton} />
|
<DeleteCard currentTenantId={currentTenantId} onClick={onClickDeletionButton} />
|
||||||
</div>
|
</div>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
@ -123,17 +125,17 @@ function TenantBasicSettings() {
|
||||||
onDiscard={reset}
|
onDiscard={reset}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
|
|
||||||
<DeleteModal
|
|
||||||
isOpen={isDeletionModalOpen}
|
|
||||||
isLoading={isDeleting}
|
|
||||||
tenant={watch('profile')}
|
|
||||||
onClose={() => {
|
|
||||||
setIsDeletionModalOpen(false);
|
|
||||||
}}
|
|
||||||
onDelete={onDelete}
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
|
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
|
||||||
|
<DeleteModal
|
||||||
|
isOpen={isDeletionModalOpen}
|
||||||
|
isLoading={isDeleting}
|
||||||
|
tenant={watch('profile')}
|
||||||
|
onClose={() => {
|
||||||
|
setIsDeletionModalOpen(false);
|
||||||
|
}}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,6 +192,7 @@
|
||||||
--color-bg-toast: var(--color-neutral-20);
|
--color-bg-toast: var(--color-neutral-20);
|
||||||
--color-bg-state-unselected: var(--color-neutral-90);
|
--color-bg-state-unselected: var(--color-neutral-90);
|
||||||
--color-bg-state-disabled: rgba(25, 28, 29, 8%); // 8% --color-neutral-10
|
--color-bg-state-disabled: rgba(25, 28, 29, 8%); // 8% --color-neutral-10
|
||||||
|
--color-bg-info-tag: rgba(229, 225, 236, 80%); // 80% --color-neutral-variant-90
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
|
@ -391,4 +392,5 @@
|
||||||
--color-bg-toast: var(--color-neutral-80);
|
--color-bg-toast: var(--color-neutral-80);
|
||||||
--color-bg-state-unselected: var(--color-neutral-90);
|
--color-bg-state-unselected: var(--color-neutral-90);
|
||||||
--color-bg-state-disabled: rgba(247, 248, 248, 8%); // 8% --color-neutral-10
|
--color-bg-state-disabled: rgba(247, 248, 248, 8%); // 8% --color-neutral-10
|
||||||
|
--color-bg-info-tag: var(--color-neutral-variant-90);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue