UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: вкладки enterprise-настроек Voice Tasker
This commit is contained in:
parent
b796a21852
commit
5e57786d39
|
|
@ -112,8 +112,22 @@ const ACCESS_MODE_OPTIONS: {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type TVoiceTaskerSettingsTab = "access" | "limits" | "models" | "monitor";
|
||||||
|
|
||||||
|
const VOICE_TASKER_SETTINGS_TABS: {
|
||||||
|
icon: ElementType;
|
||||||
|
label: string;
|
||||||
|
value: TVoiceTaskerSettingsTab;
|
||||||
|
}[] = [
|
||||||
|
{ value: "access", label: "Доступ", icon: ShieldCheck },
|
||||||
|
{ value: "limits", label: "Лимиты", icon: Activity },
|
||||||
|
{ value: "models", label: "Модели и ключ", icon: KeyRound },
|
||||||
|
{ value: "monitor", label: "Очередь и журнал", icon: BrainCircuit },
|
||||||
|
];
|
||||||
|
|
||||||
export const AIVoiceTaskerSettingsContent = observer(function AIVoiceTaskerSettingsContent(props: TProps) {
|
export const AIVoiceTaskerSettingsContent = observer(function AIVoiceTaskerSettingsContent(props: TProps) {
|
||||||
const { showHeading = true, workspaceSlug } = props;
|
const { showHeading = true, workspaceSlug } = props;
|
||||||
|
const [activeTab, setActiveTab] = useState<TVoiceTaskerSettingsTab>("access");
|
||||||
const [formState, setFormState] = useState<TFormState>(getInitialFormState());
|
const [formState, setFormState] = useState<TFormState>(getInitialFormState());
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
|
@ -325,177 +339,198 @@ export const AIVoiceTaskerSettingsContent = observer(function AIVoiceTaskerSetti
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<AccessScopeSection
|
<VoiceTaskerSettingsTabs activeTab={activeTab} onChange={setActiveTab} />
|
||||||
accessMode={formState.access_mode}
|
|
||||||
enabledMemberIds={formState.enabled_member_ids}
|
|
||||||
enabledProjectIds={formState.enabled_project_ids}
|
|
||||||
members={workspaceMembers}
|
|
||||||
projects={projects}
|
|
||||||
onAccessModeChange={(value) => updateFormValue("access_mode", value)}
|
|
||||||
onToggleMember={(memberId) => toggleListValue("enabled_member_ids", memberId)}
|
|
||||||
onToggleProject={(projectId) => toggleListValue("enabled_project_ids", projectId)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="grid gap-5 px-5 py-5 md:grid-cols-2">
|
{activeTab === "access" && (
|
||||||
<Field label="Provider">
|
<section className="nodedc-settings-card overflow-hidden">
|
||||||
<Input
|
<AccessScopeSection
|
||||||
value="OpenAI"
|
accessMode={formState.access_mode}
|
||||||
disabled
|
enabledMemberIds={formState.enabled_member_ids}
|
||||||
className="nodedc-settings-input w-full cursor-not-allowed !bg-white/4"
|
enabledProjectIds={formState.enabled_project_ids}
|
||||||
|
members={workspaceMembers}
|
||||||
|
projects={projects}
|
||||||
|
onAccessModeChange={(value) => updateFormValue("access_mode", value)}
|
||||||
|
onToggleMember={(memberId) => toggleListValue("enabled_member_ids", memberId)}
|
||||||
|
onToggleProject={(projectId) => toggleListValue("enabled_project_ids", projectId)}
|
||||||
|
/>
|
||||||
|
<div className="grid gap-5 border-t border-white/5 px-5 py-5 md:grid-cols-2">
|
||||||
|
<Field label="Default project fallback">
|
||||||
|
<select
|
||||||
|
value={formState.default_project_id}
|
||||||
|
onChange={(event) => updateFormValue("default_project_id", event.target.value)}
|
||||||
|
className="nodedc-settings-select text-sm w-full px-4"
|
||||||
|
>
|
||||||
|
<option value="">None</option>
|
||||||
|
{projects.map((project) => (
|
||||||
|
<option key={project.id} value={project.id}>
|
||||||
|
{project.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
<FeatureScopeSummary
|
||||||
|
accessMode={formState.access_mode}
|
||||||
|
enabledMemberIds={formState.enabled_member_ids}
|
||||||
|
enabledProjectIds={formState.enabled_project_ids}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</div>
|
||||||
<Field label="Default project fallback">
|
</section>
|
||||||
<select
|
)}
|
||||||
value={formState.default_project_id}
|
|
||||||
onChange={(event) => updateFormValue("default_project_id", event.target.value)}
|
{activeTab === "limits" && (
|
||||||
className="nodedc-settings-select text-sm w-full px-4"
|
<section className="nodedc-settings-card overflow-hidden">
|
||||||
|
<SectionHeader
|
||||||
|
icon={Activity}
|
||||||
|
title="Лимиты и retention"
|
||||||
|
description="Ограничения применяются до отправки аудио в OpenAI; retention очищает transcript и parser payload без удаления метрик."
|
||||||
|
/>
|
||||||
|
<LimitUsageOverview formState={formState} monitor={monitor} />
|
||||||
|
<div className="grid gap-5 border-t border-white/5 px-5 py-5 md:grid-cols-2">
|
||||||
|
<Field label="Max audio duration">
|
||||||
|
<NumberInput
|
||||||
|
value={formState.max_audio_duration_seconds}
|
||||||
|
min={10}
|
||||||
|
max={600}
|
||||||
|
suffix="seconds"
|
||||||
|
onChange={(value) => updateFormValue("max_audio_duration_seconds", value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Лимит пользователя за час">
|
||||||
|
<NumberInput
|
||||||
|
value={formState.per_user_hourly_limit}
|
||||||
|
min={1}
|
||||||
|
max={1000}
|
||||||
|
suffix="за час"
|
||||||
|
onChange={(value) => updateFormValue("per_user_hourly_limit", value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Лимит workspace за час">
|
||||||
|
<NumberInput
|
||||||
|
value={formState.workspace_hourly_limit}
|
||||||
|
min={1}
|
||||||
|
max={10000}
|
||||||
|
suffix="за час"
|
||||||
|
onChange={(value) => updateFormValue("workspace_hourly_limit", value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Суточная квота пользователя">
|
||||||
|
<NumberInput
|
||||||
|
value={formState.per_user_daily_limit}
|
||||||
|
min={1}
|
||||||
|
max={10000}
|
||||||
|
suffix="за сутки"
|
||||||
|
onChange={(value) => updateFormValue("per_user_daily_limit", value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Суточная квота workspace">
|
||||||
|
<NumberInput
|
||||||
|
value={formState.workspace_daily_limit}
|
||||||
|
min={1}
|
||||||
|
max={100000}
|
||||||
|
suffix="за сутки"
|
||||||
|
onChange={(value) => updateFormValue("workspace_daily_limit", value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Суточная квота контура">
|
||||||
|
<NumberInput
|
||||||
|
value={formState.project_daily_limit}
|
||||||
|
min={1}
|
||||||
|
max={50000}
|
||||||
|
suffix="за сутки"
|
||||||
|
onChange={(value) => updateFormValue("project_daily_limit", value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Одновременные обработки workspace">
|
||||||
|
<NumberInput
|
||||||
|
value={formState.workspace_concurrency_limit}
|
||||||
|
min={1}
|
||||||
|
max={50}
|
||||||
|
suffix="job"
|
||||||
|
onChange={(value) => updateFormValue("workspace_concurrency_limit", value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Хранение чувствительных данных">
|
||||||
|
<NumberInput
|
||||||
|
value={formState.sensitive_data_retention_days}
|
||||||
|
min={1}
|
||||||
|
max={365}
|
||||||
|
suffix="дней"
|
||||||
|
onChange={(value) => updateFormValue("sensitive_data_retention_days", value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "models" && (
|
||||||
|
<section className="nodedc-settings-card overflow-hidden">
|
||||||
|
<SectionHeader
|
||||||
|
icon={KeyRound}
|
||||||
|
title="Модели и OpenAI key"
|
||||||
|
description="Key заменяется только если ввести новый. В API response возвращается только last4."
|
||||||
|
right={
|
||||||
|
<CredentialStatus hasKey={settings.credential.has_key} keyLast4={settings.credential.key_last4} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="grid gap-5 border-t border-white/5 px-5 py-5 md:grid-cols-2">
|
||||||
|
<Field label="Provider">
|
||||||
|
<Input
|
||||||
|
value="OpenAI"
|
||||||
|
disabled
|
||||||
|
className="nodedc-settings-input w-full cursor-not-allowed !bg-white/4"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Transcription model">
|
||||||
|
<Input
|
||||||
|
value={formState.transcription_model}
|
||||||
|
onChange={(event) => updateFormValue("transcription_model", event.target.value)}
|
||||||
|
className="nodedc-settings-input w-full"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Structuring model">
|
||||||
|
<Input
|
||||||
|
value={formState.structuring_model}
|
||||||
|
onChange={(event) => updateFormValue("structuring_model", event.target.value)}
|
||||||
|
className="nodedc-settings-input w-full"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="OpenAI API Key">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
value={formState.openai_api_key}
|
||||||
|
onChange={(event) => updateFormValue("openai_api_key", event.target.value)}
|
||||||
|
placeholder={settings.credential.has_key ? "sk-... не изменять" : "sk-..."}
|
||||||
|
className="nodedc-settings-input w-full"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end px-5 pb-5">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
|
className="nodedc-settings-chip min-w-[10rem]"
|
||||||
|
loading={isTesting}
|
||||||
|
disabled={!settings.credential.has_key || isSaving}
|
||||||
|
onClick={handleTestConnection}
|
||||||
>
|
>
|
||||||
<option value="">None</option>
|
Test connection
|
||||||
{projects.map((project) => (
|
</Button>
|
||||||
<option key={project.id} value={project.id}>
|
</div>
|
||||||
{project.name}
|
</section>
|
||||||
</option>
|
)}
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</Field>
|
|
||||||
<Field label="Max audio duration">
|
|
||||||
<NumberInput
|
|
||||||
value={formState.max_audio_duration_seconds}
|
|
||||||
min={10}
|
|
||||||
max={600}
|
|
||||||
suffix="seconds"
|
|
||||||
onChange={(value) => updateFormValue("max_audio_duration_seconds", value)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="nodedc-settings-card overflow-hidden">
|
{activeTab === "monitor" && (
|
||||||
<SectionHeader
|
<VoiceTaskMonitorSection
|
||||||
icon={KeyRound}
|
isCleaning={isCleaningStaleSessions}
|
||||||
title="OpenAI credential"
|
isLoading={isMonitorLoading}
|
||||||
description="Key заменяется только если ввести новый. В API response возвращается только last4."
|
isRunningRetention={isRunningRetention}
|
||||||
right={<CredentialStatus hasKey={settings.credential.has_key} keyLast4={settings.credential.key_last4} />}
|
monitor={monitor}
|
||||||
|
onCleanupStale={handleCleanupStaleSessions}
|
||||||
|
onRunRetention={handleRunRetention}
|
||||||
/>
|
/>
|
||||||
<div className="grid gap-5 px-5 py-5 md:grid-cols-[1fr_auto] md:items-end">
|
)}
|
||||||
<Field label="OpenAI API Key">
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
value={formState.openai_api_key}
|
|
||||||
onChange={(event) => updateFormValue("openai_api_key", event.target.value)}
|
|
||||||
placeholder={settings.credential.has_key ? "sk-... не изменять" : "sk-..."}
|
|
||||||
className="nodedc-settings-input w-full"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="lg"
|
|
||||||
className="nodedc-settings-chip min-w-[10rem]"
|
|
||||||
loading={isTesting}
|
|
||||||
disabled={!settings.credential.has_key || isSaving}
|
|
||||||
onClick={handleTestConnection}
|
|
||||||
>
|
|
||||||
Test connection
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="nodedc-settings-card overflow-hidden">
|
|
||||||
<SectionHeader
|
|
||||||
icon={BrainCircuit}
|
|
||||||
title="Models and limits"
|
|
||||||
description="Один workspace key обслуживает всех пользователей. Лимиты срабатывают до отправки аудио в OpenAI."
|
|
||||||
/>
|
|
||||||
<div className="grid gap-5 px-5 py-5 md:grid-cols-2">
|
|
||||||
<Field label="Transcription model">
|
|
||||||
<Input
|
|
||||||
value={formState.transcription_model}
|
|
||||||
onChange={(event) => updateFormValue("transcription_model", event.target.value)}
|
|
||||||
className="nodedc-settings-input w-full"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Structuring model">
|
|
||||||
<Input
|
|
||||||
value={formState.structuring_model}
|
|
||||||
onChange={(event) => updateFormValue("structuring_model", event.target.value)}
|
|
||||||
className="nodedc-settings-input w-full"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Лимит пользователя за час">
|
|
||||||
<NumberInput
|
|
||||||
value={formState.per_user_hourly_limit}
|
|
||||||
min={1}
|
|
||||||
max={1000}
|
|
||||||
suffix="за час"
|
|
||||||
onChange={(value) => updateFormValue("per_user_hourly_limit", value)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Лимит workspace за час">
|
|
||||||
<NumberInput
|
|
||||||
value={formState.workspace_hourly_limit}
|
|
||||||
min={1}
|
|
||||||
max={10000}
|
|
||||||
suffix="за час"
|
|
||||||
onChange={(value) => updateFormValue("workspace_hourly_limit", value)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Суточная квота пользователя">
|
|
||||||
<NumberInput
|
|
||||||
value={formState.per_user_daily_limit}
|
|
||||||
min={1}
|
|
||||||
max={10000}
|
|
||||||
suffix="за сутки"
|
|
||||||
onChange={(value) => updateFormValue("per_user_daily_limit", value)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Суточная квота workspace">
|
|
||||||
<NumberInput
|
|
||||||
value={formState.workspace_daily_limit}
|
|
||||||
min={1}
|
|
||||||
max={100000}
|
|
||||||
suffix="за сутки"
|
|
||||||
onChange={(value) => updateFormValue("workspace_daily_limit", value)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Суточная квота контура">
|
|
||||||
<NumberInput
|
|
||||||
value={formState.project_daily_limit}
|
|
||||||
min={1}
|
|
||||||
max={50000}
|
|
||||||
suffix="за сутки"
|
|
||||||
onChange={(value) => updateFormValue("project_daily_limit", value)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Одновременные обработки workspace">
|
|
||||||
<NumberInput
|
|
||||||
value={formState.workspace_concurrency_limit}
|
|
||||||
min={1}
|
|
||||||
max={50}
|
|
||||||
suffix="job"
|
|
||||||
onChange={(value) => updateFormValue("workspace_concurrency_limit", value)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Хранение чувствительных данных">
|
|
||||||
<NumberInput
|
|
||||||
value={formState.sensitive_data_retention_days}
|
|
||||||
min={1}
|
|
||||||
max={365}
|
|
||||||
suffix="дней"
|
|
||||||
onChange={(value) => updateFormValue("sensitive_data_retention_days", value)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<VoiceTaskMonitorSection
|
|
||||||
isCleaning={isCleaningStaleSessions}
|
|
||||||
isLoading={isMonitorLoading}
|
|
||||||
isRunningRetention={isRunningRetention}
|
|
||||||
monitor={monitor}
|
|
||||||
onCleanupStale={handleCleanupStaleSessions}
|
|
||||||
onRunRetention={handleRunRetention}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-3">
|
<div className="flex items-center justify-end gap-3">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -531,6 +566,128 @@ type TAccessScopeSectionProps = {
|
||||||
projects: IProject[];
|
projects: IProject[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TVoiceTaskerSettingsTabsProps = {
|
||||||
|
activeTab: TVoiceTaskerSettingsTab;
|
||||||
|
onChange: (value: TVoiceTaskerSettingsTab) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function VoiceTaskerSettingsTabs({ activeTab, onChange }: TVoiceTaskerSettingsTabsProps) {
|
||||||
|
return (
|
||||||
|
<div className="nodedc-settings-card flex flex-wrap gap-2 p-2">
|
||||||
|
{VOICE_TASKER_SETTINGS_TABS.map((tab) => {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
const isActive = activeTab === tab.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tab.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onChange(tab.value)}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex min-h-10 flex-1 items-center justify-center gap-2 rounded-full px-4 text-13 font-semibold transition-colors md:flex-none",
|
||||||
|
isActive
|
||||||
|
? "bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))]"
|
||||||
|
: "bg-white/[0.045] text-secondary hover:bg-white/[0.07] hover:text-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="size-4" />
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type TFeatureScopeSummaryProps = {
|
||||||
|
accessMode: TWorkspaceAIAccessMode;
|
||||||
|
enabledMemberIds: string[];
|
||||||
|
enabledProjectIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function FeatureScopeSummary({ accessMode, enabledMemberIds, enabledProjectIds }: TFeatureScopeSummaryProps) {
|
||||||
|
const summaryByMode: Record<TWorkspaceAIAccessMode, { label: string; value: string }> = {
|
||||||
|
all_workspace_members: { label: "Активный scope", value: "Весь workspace" },
|
||||||
|
admins_only: { label: "Активный scope", value: "Только админы" },
|
||||||
|
selected_projects: { label: "Выбранные контуры", value: formatMonitorNumber(enabledProjectIds.length) },
|
||||||
|
selected_members: { label: "Выбранные пользователи", value: formatMonitorNumber(enabledMemberIds.length) },
|
||||||
|
};
|
||||||
|
const summary = summaryByMode[accessMode];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-[1.35rem] bg-white/[0.045] px-4 py-3">
|
||||||
|
<div className="text-11 font-semibold tracking-[0.18em] text-tertiary uppercase">{summary.label}</div>
|
||||||
|
<div className="mt-2 text-2xl font-semibold text-[rgb(var(--nodedc-accent-rgb))]">{summary.value}</div>
|
||||||
|
<div className="mt-1 text-11 text-tertiary">backend проверка перед записью и публикацией</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type TLimitUsageOverviewProps = {
|
||||||
|
formState: TFormState;
|
||||||
|
monitor?: TVoiceTaskMonitor;
|
||||||
|
};
|
||||||
|
|
||||||
|
function LimitUsageOverview({ formState, monitor }: TLimitUsageOverviewProps) {
|
||||||
|
const totalSessions = monitor?.summary.total ?? 0;
|
||||||
|
const activeSessions = monitor?.summary.active ?? 0;
|
||||||
|
const workspaceDailyRatio = getLimitRatio(totalSessions, formState.workspace_daily_limit);
|
||||||
|
const concurrencyRatio = getLimitRatio(activeSessions, formState.workspace_concurrency_limit);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-3 border-t border-white/5 px-5 py-5 md:grid-cols-4">
|
||||||
|
<LimitUsageCard
|
||||||
|
label="Workspace / сутки"
|
||||||
|
value={`${formatMonitorNumber(totalSessions)} / ${formatMonitorNumber(formState.workspace_daily_limit)}`}
|
||||||
|
ratio={workspaceDailyRatio}
|
||||||
|
/>
|
||||||
|
<LimitUsageCard
|
||||||
|
label="Очередь сейчас"
|
||||||
|
value={`${formatMonitorNumber(activeSessions)} / ${formatMonitorNumber(formState.workspace_concurrency_limit)}`}
|
||||||
|
ratio={concurrencyRatio}
|
||||||
|
/>
|
||||||
|
<LimitUsageCard
|
||||||
|
label="Project / сутки"
|
||||||
|
value={formatMonitorNumber(formState.project_daily_limit)}
|
||||||
|
ratio={0}
|
||||||
|
/>
|
||||||
|
<LimitUsageCard
|
||||||
|
label="Retention"
|
||||||
|
value={`${formatMonitorNumber(formState.sensitive_data_retention_days)} дней`}
|
||||||
|
ratio={0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type TLimitUsageCardProps = {
|
||||||
|
label: string;
|
||||||
|
ratio: number;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function LimitUsageCard({ label, ratio, value }: TLimitUsageCardProps) {
|
||||||
|
const boundedRatio = Math.min(Math.max(ratio, 0), 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-[1.35rem] bg-white/[0.045] px-4 py-3">
|
||||||
|
<div className="text-11 font-semibold tracking-[0.18em] text-tertiary uppercase">{label}</div>
|
||||||
|
<div className="mt-2 text-xl font-semibold text-primary">{value}</div>
|
||||||
|
<div className="mt-3 h-2 overflow-hidden rounded-full bg-white/8">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-[rgb(var(--nodedc-accent-rgb))]"
|
||||||
|
style={{ width: `${Math.round(boundedRatio * 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLimitRatio(value: number, limit: number) {
|
||||||
|
if (!limit || limit <= 0) return 0;
|
||||||
|
return value / limit;
|
||||||
|
}
|
||||||
|
|
||||||
function AccessScopeSection({
|
function AccessScopeSection({
|
||||||
accessMode,
|
accessMode,
|
||||||
enabledMemberIds,
|
enabledMemberIds,
|
||||||
|
|
@ -545,7 +702,7 @@ function AccessScopeSection({
|
||||||
const selectedMembersCount = enabledMemberIds.length;
|
const selectedMembersCount = enabledMemberIds.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-white/5 px-5 py-5">
|
<div className="px-5 py-5">
|
||||||
<div className="mb-4 flex flex-col gap-1">
|
<div className="mb-4 flex flex-col gap-1">
|
||||||
<h4 className="text-13 font-semibold text-primary">Доступ к функции</h4>
|
<h4 className="text-13 font-semibold text-primary">Доступ к функции</h4>
|
||||||
<p className="max-w-3xl text-12 leading-5 text-tertiary">
|
<p className="max-w-3xl text-12 leading-5 text-tertiary">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue