typescript
This commit is contained in:
+107
-33
@@ -1,9 +1,66 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { ChangeEvent, FormEvent } from 'react';
|
||||
|
||||
type Identifier = string | number;
|
||||
|
||||
interface ApiTokenEntry {
|
||||
id?: Identifier;
|
||||
label?: string;
|
||||
expires_at?: string;
|
||||
created_at?: string;
|
||||
last_used_at?: string;
|
||||
revoked_at?: string;
|
||||
revoked_reason?: string;
|
||||
revokedReason?: string;
|
||||
transports?: string[];
|
||||
capability_set_id?: Identifier;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface CapabilitySetEntry {
|
||||
id?: Identifier;
|
||||
slug?: string;
|
||||
label?: string;
|
||||
capabilities?: string[];
|
||||
}
|
||||
|
||||
interface CapabilitySetOption {
|
||||
value: string;
|
||||
label: string;
|
||||
capabilities: string[];
|
||||
sourceId?: Identifier;
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
interface CapabilitySelectionOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface CopyFeedbackState {
|
||||
type: 'success' | 'error';
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ApiTokensSectionProps {
|
||||
tokens?: ApiTokenEntry[];
|
||||
loading?: boolean;
|
||||
creating?: boolean;
|
||||
deletingId?: Identifier | null;
|
||||
regeneratingId?: Identifier | null;
|
||||
createdToken?: string | null;
|
||||
capabilitySets?: CapabilitySetEntry[];
|
||||
capabilitySetsLoading?: boolean;
|
||||
capabilities?: string[];
|
||||
capabilitiesLoading?: boolean;
|
||||
onRefresh?: () => void;
|
||||
onCreate?: (payload: { label?: string; expires_at?: string; capability_set_id: Identifier | string }) => Promise<unknown> | unknown;
|
||||
onDelete?: (tokenId: Identifier) => Promise<unknown> | unknown;
|
||||
onRegenerate?: (tokenId: Identifier) => Promise<unknown> | unknown;
|
||||
onDismissCreatedToken?: () => void;
|
||||
onRefreshCapabilitySets?: () => void;
|
||||
onRefreshCapabilities?: () => void;
|
||||
}
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
if (!value) {
|
||||
@@ -34,34 +91,42 @@ const ApiTokensSection = ({
|
||||
onDismissCreatedToken,
|
||||
onRefreshCapabilitySets,
|
||||
onRefreshCapabilities,
|
||||
}) => {
|
||||
}: ApiTokensSectionProps) => {
|
||||
const [newTokenLabel, setNewTokenLabel] = useState('');
|
||||
const [newTokenExpires, setNewTokenExpires] = useState('');
|
||||
const [newTokenCapabilitySetId, setNewTokenCapabilitySetId] = useState('');
|
||||
const [formError, setFormError] = useState(null);
|
||||
const supportsClipboardWrite = Boolean(navigator?.clipboard?.writeText);
|
||||
const [canCopyToken, setCanCopyToken] = useState(supportsClipboardWrite);
|
||||
const [copyFeedback, setCopyFeedback] = useState(null);
|
||||
const [newTokenCapabilitySetId, setNewTokenCapabilitySetId] = useState<string>('');
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const supportsClipboardWrite = typeof navigator !== 'undefined' && Boolean(navigator?.clipboard?.writeText);
|
||||
const [canCopyToken, setCanCopyToken] = useState<boolean>(supportsClipboardWrite);
|
||||
const [copyFeedback, setCopyFeedback] = useState<CopyFeedbackState | null>(null);
|
||||
|
||||
const capabilitySetOptions = useMemo(
|
||||
() => capabilitySets.map((set) => ({
|
||||
value: set.id,
|
||||
label: set.label || set.slug || set.id,
|
||||
capabilities: Array.isArray(set.capabilities) ? set.capabilities : [],
|
||||
})),
|
||||
const capabilitySetOptions = useMemo<CapabilitySetOption[]>(
|
||||
() =>
|
||||
capabilitySets.map((set) => ({
|
||||
value: set.id != null ? String(set.id) : set.slug ?? '',
|
||||
label: `${set.label || set.slug || set.id || 'Capability set'}`,
|
||||
capabilities: Array.isArray(set.capabilities)
|
||||
? set.capabilities.map((cap) => String(cap ?? ''))
|
||||
: [],
|
||||
sourceId: set.id,
|
||||
slug: set.slug,
|
||||
})),
|
||||
[capabilitySets],
|
||||
);
|
||||
|
||||
const capabilitySetMap = useMemo(
|
||||
() => Object.fromEntries(capabilitySetOptions.map((option) => [option.value, option])),
|
||||
const capabilitySetMap = useMemo<Record<string, CapabilitySetOption>>(
|
||||
() => {
|
||||
const entries = capabilitySetOptions.map((option) => [option.value, option] as const);
|
||||
return Object.fromEntries(entries);
|
||||
},
|
||||
[capabilitySetOptions],
|
||||
);
|
||||
|
||||
const capabilitySelectionOptions = useMemo(() => (
|
||||
const capabilitySelectionOptions = useMemo<CapabilitySelectionOption[]>(() => (
|
||||
Array.isArray(capabilities)
|
||||
? capabilities.map((capability) => {
|
||||
if (typeof capability?.split !== 'function') {
|
||||
return { value: capability, label: String(capability) };
|
||||
if (typeof capability !== 'string') {
|
||||
return { value: String(capability ?? ''), label: String(capability ?? '') };
|
||||
}
|
||||
const [namespace, action] = capability.split(':');
|
||||
if (!namespace || !action) {
|
||||
@@ -78,14 +143,14 @@ const ApiTokensSection = ({
|
||||
), [capabilities]);
|
||||
|
||||
const capabilityLabelMap = useMemo(() => {
|
||||
const map = new Map();
|
||||
const map = new Map<string, string>();
|
||||
capabilitySelectionOptions.forEach(({ value, label }) => {
|
||||
map.set(value, label || String(value));
|
||||
});
|
||||
return map;
|
||||
}, [capabilitySelectionOptions]);
|
||||
|
||||
const formatCapabilityLabel = useCallback((value) => (
|
||||
const formatCapabilityLabel = useCallback((value: string) => (
|
||||
capabilityLabelMap.get(value) || String(value)
|
||||
), [capabilityLabelMap]);
|
||||
|
||||
@@ -128,6 +193,9 @@ const ApiTokensSection = ({
|
||||
});
|
||||
|
||||
try {
|
||||
if (typeof navigator === 'undefined') {
|
||||
throw new Error('Clipboard API unavailable');
|
||||
}
|
||||
await navigator.clipboard.writeText(createdToken);
|
||||
showSuccess();
|
||||
return;
|
||||
@@ -146,16 +214,17 @@ const ApiTokensSection = ({
|
||||
|
||||
useEffect(() => {
|
||||
setCopyFeedback(null);
|
||||
setCanCopyToken(Boolean(navigator?.clipboard?.writeText));
|
||||
const hasClipboard = typeof navigator !== 'undefined' && Boolean(navigator?.clipboard?.writeText);
|
||||
setCanCopyToken(hasClipboard);
|
||||
}, [createdToken]);
|
||||
|
||||
const handleNewCapabilitySetChange = useCallback((event) => {
|
||||
const handleNewCapabilitySetChange = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
|
||||
setFormError(null);
|
||||
setNewTokenCapabilitySetId(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleCreateToken = useCallback(
|
||||
async (event) => {
|
||||
async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setFormError(null);
|
||||
let normalizedLabel = newTokenLabel.trim();
|
||||
@@ -184,10 +253,12 @@ const ApiTokensSection = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedCapabilitySetId = capabilitySetMap[selectedCapabilitySetId]?.sourceId ?? selectedCapabilitySetId;
|
||||
|
||||
const result = await onCreate?.({
|
||||
label: normalizedLabel,
|
||||
expires_at: normalizedExpires,
|
||||
capability_set_id: selectedCapabilitySetId,
|
||||
capability_set_id: resolvedCapabilitySetId,
|
||||
});
|
||||
|
||||
if (result !== false) {
|
||||
@@ -199,6 +270,7 @@ const ApiTokensSection = ({
|
||||
},
|
||||
[
|
||||
capabilitySetOptions,
|
||||
capabilitySetMap,
|
||||
newTokenCapabilitySetId,
|
||||
newTokenExpires,
|
||||
newTokenLabel,
|
||||
@@ -207,7 +279,7 @@ const ApiTokensSection = ({
|
||||
);
|
||||
|
||||
const handleRegenerateToken = useCallback(
|
||||
async (token) => {
|
||||
async (token: ApiTokenEntry) => {
|
||||
if (!token?.id) {
|
||||
return;
|
||||
}
|
||||
@@ -359,9 +431,10 @@ const ApiTokensSection = ({
|
||||
<tbody>
|
||||
{tokens.map((token) => {
|
||||
const isRevoked = Boolean(token?.revoked_at);
|
||||
const selectedSet = token?.capability_set_id
|
||||
? capabilitySetMap[token.capability_set_id]
|
||||
const capabilityKey = token?.capability_set_id != null
|
||||
? String(token.capability_set_id)
|
||||
: null;
|
||||
const selectedSet = capabilityKey ? capabilitySetMap[capabilityKey] : null;
|
||||
const capabilitySetLabel = selectedSet?.label
|
||||
|| selectedSet?.slug
|
||||
|| token.capability_set_id
|
||||
@@ -399,9 +472,10 @@ const ApiTokensSection = ({
|
||||
<button
|
||||
type="button"
|
||||
className="danger"
|
||||
onClick={() => onDelete?.(token.id)}
|
||||
onClick={() => token.id && onDelete?.(token.id)}
|
||||
disabled={
|
||||
deletingId === token.id || regeneratingId === token.id
|
||||
|| !token.id
|
||||
}
|
||||
>
|
||||
{deletingId === token.id ? 'Revoking…' : 'Revoke'}
|
||||
+111
-44
@@ -1,13 +1,76 @@
|
||||
import React, {
|
||||
FormEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { SettingsSectionConfig } from '../SettingsModal';
|
||||
import { IconX } from '../../ui/icons';
|
||||
import CapabilityDropdown from '../components/CapabilityDropdown';
|
||||
import CapabilityDropdown, { CapabilityDropdownOption } from '../components/CapabilityDropdown';
|
||||
|
||||
const CapabilitySetsSection = ({
|
||||
type CapabilityValue = string | number;
|
||||
type CapabilitySetId = string | number;
|
||||
|
||||
interface CapabilitySet {
|
||||
id: CapabilitySetId;
|
||||
slug?: string;
|
||||
label?: string;
|
||||
capabilities?: CapabilityValue[] | null;
|
||||
is_system?: boolean;
|
||||
cap_version?: number | null;
|
||||
}
|
||||
|
||||
interface CapabilitySetMutationPayload {
|
||||
slug?: string;
|
||||
label?: string;
|
||||
capabilities: CapabilityValue[];
|
||||
}
|
||||
|
||||
type RefreshHandler = (() => void | Promise<void>) | undefined;
|
||||
type CreateCapabilitySetHandler = (payload: CapabilitySetMutationPayload) => Promise<boolean | CapabilitySet | void | null | undefined> | boolean | CapabilitySet | void | null | undefined;
|
||||
type UpdateCapabilitySetHandler = (
|
||||
id: CapabilitySetId,
|
||||
payload: CapabilitySetMutationPayload,
|
||||
) => Promise<boolean | CapabilitySet | void | null | undefined> | boolean | CapabilitySet | void | null | undefined;
|
||||
type DeleteCapabilitySetHandler = (id: CapabilitySetId) => Promise<boolean | void | null | undefined> | boolean | void | null | undefined;
|
||||
|
||||
interface CapabilitySetsSectionProps {
|
||||
capabilitySets?: CapabilitySet[];
|
||||
capabilitySetsLoading?: boolean;
|
||||
creatingCapabilitySet?: boolean;
|
||||
savingCapabilitySetId?: CapabilitySetId | null;
|
||||
deletingCapabilitySetId?: CapabilitySetId | null;
|
||||
supportsCapabilitySetLabels?: boolean;
|
||||
capabilities?: CapabilityValue[];
|
||||
capabilitiesLoading?: boolean;
|
||||
onRefreshCapabilitySets?: RefreshHandler;
|
||||
onRefreshCapabilities?: RefreshHandler;
|
||||
onRefresh?: RefreshHandler;
|
||||
onCreateCapabilitySet?: CreateCapabilitySetHandler;
|
||||
onUpdateCapabilitySet?: UpdateCapabilitySetHandler;
|
||||
onDeleteCapabilitySet?: DeleteCapabilitySetHandler;
|
||||
}
|
||||
|
||||
type CapabilityOptionInput = CapabilityDropdownOption | CapabilityValue | null | undefined;
|
||||
|
||||
const resolveCapabilityValue = (option: CapabilityOptionInput): CapabilityValue | null => {
|
||||
if (option == null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof option === 'string' || typeof option === 'number') {
|
||||
return option;
|
||||
}
|
||||
if (option.value != null) {
|
||||
return option.value as CapabilityValue;
|
||||
}
|
||||
if (option.id != null) {
|
||||
return option.id as CapabilityValue;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const CapabilitySetsSection: React.FC<CapabilitySetsSectionProps> = ({
|
||||
capabilitySets = [],
|
||||
capabilitySetsLoading = false,
|
||||
creatingCapabilitySet = false,
|
||||
@@ -25,58 +88,62 @@ const CapabilitySetsSection = ({
|
||||
}) => {
|
||||
const [newCapabilitySetSlug, setNewCapabilitySetSlug] = useState('');
|
||||
const [newCapabilitySetLabel, setNewCapabilitySetLabel] = useState('');
|
||||
const [newCapabilitySetCapabilities, setNewCapabilitySetCapabilities] = useState([]);
|
||||
const [capabilitySetFormError, setCapabilitySetFormError] = useState(null);
|
||||
const [newCapabilitySetCapabilities, setNewCapabilitySetCapabilities] = useState<CapabilityValue[]>([]);
|
||||
const [capabilitySetFormError, setCapabilitySetFormError] = useState<string | null>(null);
|
||||
|
||||
const [editingCapabilitySetId, setEditingCapabilitySetId] = useState(null);
|
||||
const [editingCapabilitySetId, setEditingCapabilitySetId] = useState<CapabilitySetId | null>(null);
|
||||
const [editCapabilitySetSlug, setEditCapabilitySetSlug] = useState('');
|
||||
const [editCapabilitySetLabel, setEditCapabilitySetLabel] = useState('');
|
||||
const [editCapabilitySetCapabilities, setEditCapabilitySetCapabilities] = useState([]);
|
||||
const [capabilitySetEditError, setCapabilitySetEditError] = useState(null);
|
||||
const [editCapabilitySetCapabilities, setEditCapabilitySetCapabilities] = useState<CapabilityValue[]>([]);
|
||||
const [capabilitySetEditError, setCapabilitySetEditError] = useState<string | null>(null);
|
||||
|
||||
const capabilitySelectionOptions = useMemo(() => (
|
||||
Array.isArray(capabilities)
|
||||
? capabilities.map((capability) => {
|
||||
if (typeof capability?.split !== 'function') {
|
||||
return { value: capability, label: String(capability) };
|
||||
}
|
||||
const [namespace, action] = capability.split(':');
|
||||
if (!namespace || !action) {
|
||||
return { value: capability, label: capability };
|
||||
}
|
||||
const formattedNamespace = `${namespace.charAt(0).toUpperCase()}${namespace.slice(1)}`;
|
||||
const formattedAction = action.replace(/_/g, ' ');
|
||||
return {
|
||||
value: capability,
|
||||
label: `${formattedNamespace}: ${formattedAction}`,
|
||||
};
|
||||
})
|
||||
: []
|
||||
const capabilitySelectionOptions = useMemo<CapabilityDropdownOption[]>(() => (
|
||||
(capabilities ?? []).map((capability) => {
|
||||
if (typeof capability !== 'string') {
|
||||
return { value: capability, label: String(capability) };
|
||||
}
|
||||
const [namespace, action] = capability.split(':');
|
||||
if (!namespace || !action) {
|
||||
return { value: capability, label: capability };
|
||||
}
|
||||
const formattedNamespace = `${namespace.charAt(0).toUpperCase()}${namespace.slice(1)}`;
|
||||
const formattedAction = action.replace(/_/g, ' ');
|
||||
return {
|
||||
value: capability,
|
||||
label: `${formattedNamespace}: ${formattedAction}`,
|
||||
};
|
||||
})
|
||||
), [capabilities]);
|
||||
|
||||
const capabilityLabelMap = useMemo(() => {
|
||||
const map = new Map();
|
||||
const map = new Map<CapabilityValue, string>();
|
||||
capabilitySelectionOptions.forEach(({ value, label }) => {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
map.set(value, label || String(value));
|
||||
});
|
||||
return map;
|
||||
}, [capabilitySelectionOptions]);
|
||||
|
||||
const capabilityOrder = useMemo(() => {
|
||||
const order = new Map();
|
||||
const order = new Map<CapabilityValue, number>();
|
||||
capabilitySelectionOptions.forEach((option, index) => {
|
||||
if (option.value == null) {
|
||||
return;
|
||||
}
|
||||
order.set(option.value, index);
|
||||
});
|
||||
return order;
|
||||
}, [capabilitySelectionOptions]);
|
||||
|
||||
const sortCapabilityValues = useCallback((values) => {
|
||||
const sortCapabilityValues = useCallback((values: CapabilityValue[] | null | undefined) => {
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
return [...values].sort((a, b) => {
|
||||
const indexA = capabilityOrder.has(a) ? capabilityOrder.get(a) : Number.MAX_SAFE_INTEGER;
|
||||
const indexB = capabilityOrder.has(b) ? capabilityOrder.get(b) : Number.MAX_SAFE_INTEGER;
|
||||
const indexA = capabilityOrder.has(a) ? capabilityOrder.get(a)! : Number.MAX_SAFE_INTEGER;
|
||||
const indexB = capabilityOrder.has(b) ? capabilityOrder.get(b)! : Number.MAX_SAFE_INTEGER;
|
||||
if (indexA === indexB) {
|
||||
return String(a).localeCompare(String(b));
|
||||
}
|
||||
@@ -84,7 +151,7 @@ const CapabilitySetsSection = ({
|
||||
});
|
||||
}, [capabilityOrder]);
|
||||
|
||||
const formatCapabilityLabel = useCallback((value) => (
|
||||
const formatCapabilityLabel = useCallback((value: CapabilityValue) => (
|
||||
capabilityLabelMap.get(value) || String(value)
|
||||
), [capabilityLabelMap]);
|
||||
|
||||
@@ -111,8 +178,8 @@ const CapabilitySetsSection = ({
|
||||
onRefreshCapabilities?.();
|
||||
}, [onRefresh, onRefreshCapabilities, onRefreshCapabilitySets]);
|
||||
|
||||
const handleAddCapabilityToNewSet = useCallback((option) => {
|
||||
const value = option?.value ?? option?.id ?? option;
|
||||
const handleAddCapabilityToNewSet = useCallback((option: CapabilityOptionInput) => {
|
||||
const value = resolveCapabilityValue(option);
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
@@ -125,13 +192,13 @@ const CapabilitySetsSection = ({
|
||||
});
|
||||
}, [sortCapabilityValues]);
|
||||
|
||||
const handleRemoveCapabilityFromNewSet = useCallback((value) => {
|
||||
const handleRemoveCapabilityFromNewSet = useCallback((value: CapabilityValue) => {
|
||||
setCapabilitySetFormError(null);
|
||||
setNewCapabilitySetCapabilities((previous) => previous.filter((item) => item !== value));
|
||||
}, []);
|
||||
|
||||
const handleCreateCapabilitySetSubmit = useCallback(
|
||||
async (event) => {
|
||||
async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setCapabilitySetFormError(null);
|
||||
|
||||
@@ -145,7 +212,7 @@ const CapabilitySetsSection = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
const payload: CapabilitySetMutationPayload = {
|
||||
slug: newCapabilitySetSlug,
|
||||
label: newCapabilitySetLabel,
|
||||
capabilities: sortCapabilityValues(newCapabilitySetCapabilities),
|
||||
@@ -172,7 +239,7 @@ const CapabilitySetsSection = ({
|
||||
],
|
||||
);
|
||||
|
||||
const handleStartEditCapabilitySet = useCallback((capabilitySet) => {
|
||||
const handleStartEditCapabilitySet = useCallback((capabilitySet: CapabilitySet | null) => {
|
||||
if (!capabilitySet) {
|
||||
return;
|
||||
}
|
||||
@@ -193,8 +260,8 @@ const CapabilitySetsSection = ({
|
||||
setCapabilitySetEditError(null);
|
||||
}, []);
|
||||
|
||||
const handleAddCapabilityToEditSet = useCallback((option) => {
|
||||
const value = option?.value ?? option?.id ?? option;
|
||||
const handleAddCapabilityToEditSet = useCallback((option: CapabilityOptionInput) => {
|
||||
const value = resolveCapabilityValue(option);
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
@@ -207,13 +274,13 @@ const CapabilitySetsSection = ({
|
||||
});
|
||||
}, [sortCapabilityValues]);
|
||||
|
||||
const handleRemoveCapabilityFromEditSet = useCallback((value) => {
|
||||
const handleRemoveCapabilityFromEditSet = useCallback((value: CapabilityValue) => {
|
||||
setCapabilitySetEditError(null);
|
||||
setEditCapabilitySetCapabilities((previous) => previous.filter((item) => item !== value));
|
||||
}, []);
|
||||
|
||||
const handleUpdateCapabilitySetSubmit = useCallback(
|
||||
async (event) => {
|
||||
async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!editingCapabilitySetId) {
|
||||
return;
|
||||
@@ -223,7 +290,7 @@ const CapabilitySetsSection = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
const payload: CapabilitySetMutationPayload = {
|
||||
slug: editCapabilitySetSlug,
|
||||
label: editCapabilitySetLabel,
|
||||
capabilities: sortCapabilityValues(editCapabilitySetCapabilities),
|
||||
@@ -249,7 +316,7 @@ const CapabilitySetsSection = ({
|
||||
);
|
||||
|
||||
const handleDeleteCapabilitySet = useCallback(
|
||||
async (capabilitySet) => {
|
||||
async (capabilitySet: CapabilitySet | null) => {
|
||||
if (!capabilitySet?.id) {
|
||||
return;
|
||||
}
|
||||
@@ -573,7 +640,7 @@ const CapabilitySetsSection = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const CAPABILITY_SETS_SECTION = {
|
||||
export const CAPABILITY_SETS_SECTION: SettingsSectionConfig = {
|
||||
id: 'capabilitySets',
|
||||
label: 'Capability sets',
|
||||
component: CapabilitySetsSection,
|
||||
+16
-7
@@ -1,8 +1,6 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import type { FormEvent } from 'react';
|
||||
import type { PasskeyRecord, RegisterPasskeyResult } from '../usePasskeys';
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
if (!value) {
|
||||
@@ -15,6 +13,17 @@ const formatDateTime = (value) => {
|
||||
return timestamp.toLocaleString();
|
||||
};
|
||||
|
||||
interface PasskeysSectionProps {
|
||||
passkeys?: PasskeyRecord[];
|
||||
passkeysSupported?: boolean | null;
|
||||
passkeysLoading?: boolean;
|
||||
registeringPasskey?: boolean;
|
||||
revokingPasskeyId?: string | number | null;
|
||||
onRefreshPasskeys?: () => void | Promise<void>;
|
||||
onRegisterPasskey?: (args: { nickname?: string }) => Promise<RegisterPasskeyResult | undefined>;
|
||||
onRevokePasskey?: (id: string | number, reason?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const PasskeysSection = ({
|
||||
passkeys = [],
|
||||
passkeysSupported = null,
|
||||
@@ -24,7 +33,7 @@ const PasskeysSection = ({
|
||||
onRefreshPasskeys,
|
||||
onRegisterPasskey,
|
||||
onRevokePasskey,
|
||||
}) => {
|
||||
}: PasskeysSectionProps) => {
|
||||
const [newPasskeyNickname, setNewPasskeyNickname] = useState('');
|
||||
|
||||
const hasPasskeys = useMemo(() => Array.isArray(passkeys) && passkeys.length > 0, [passkeys]);
|
||||
@@ -34,7 +43,7 @@ const PasskeysSection = ({
|
||||
}, [onRefreshPasskeys]);
|
||||
|
||||
const handlePasskeyRegister = useCallback(
|
||||
async (event) => {
|
||||
async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const nickname = newPasskeyNickname.trim();
|
||||
const result = await onRegisterPasskey?.({ nickname });
|
||||
Reference in New Issue
Block a user