mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
chore(admin): remove useless config diff (#12545)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a GraphQL mutation to validate multiple app configuration updates, returning detailed validation results for each item. - Extended the API schema to support validation feedback, enabling client-side checks before applying changes. - Introduced a detailed, parameterized error message system for configuration validation errors. - Enabled validation of configuration inputs via the admin UI with clear, descriptive error messages. - **Improvements** - Enhanced error reporting with specific, context-rich messages for invalid app configurations. - Simplified admin settings UI by removing the confirmation dialog and streamlining save actions. - Improved clarity and maintainability of validation logic and error handling components. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -7,16 +7,16 @@ import {
|
||||
SelectValue,
|
||||
} from '@affine/admin/components/ui/select';
|
||||
import { Switch } from '@affine/admin/components/ui/switch';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { Textarea } from '../../components/ui/textarea';
|
||||
import { isEqual } from './utils';
|
||||
|
||||
export type ConfigInputProps = {
|
||||
field: string;
|
||||
desc: string;
|
||||
defaultValue: any;
|
||||
onChange: (key: string, value: any) => void;
|
||||
onChange: (field: string, value: any) => void;
|
||||
error?: string;
|
||||
} & (
|
||||
| {
|
||||
type: 'String' | 'Number' | 'Boolean' | 'JSON';
|
||||
@@ -33,6 +33,7 @@ const Inputs: Record<
|
||||
defaultValue: any;
|
||||
onChange: (value?: any) => void;
|
||||
options?: string[];
|
||||
error?: string;
|
||||
}>
|
||||
> = {
|
||||
Boolean: function SwitchInput({ defaultValue, onChange }) {
|
||||
@@ -114,22 +115,18 @@ export const ConfigRow = ({
|
||||
type,
|
||||
defaultValue,
|
||||
onChange,
|
||||
error,
|
||||
...props
|
||||
}: ConfigInputProps) => {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
|
||||
const isValueChanged = !isEqual(value, defaultValue);
|
||||
const Input = Inputs[type] ?? Inputs.JSON;
|
||||
|
||||
const onValueChange = useCallback(
|
||||
(value?: any) => {
|
||||
onChange(field, value);
|
||||
setValue(value);
|
||||
},
|
||||
[field, onChange]
|
||||
);
|
||||
|
||||
const Input = Inputs[type] ?? Inputs.JSON;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex justify-between flex-grow space-y-[10px]
|
||||
@@ -140,28 +137,12 @@ export const ConfigRow = ({
|
||||
<Input
|
||||
defaultValue={defaultValue}
|
||||
onChange={onValueChange}
|
||||
error={error}
|
||||
{...props}
|
||||
/>
|
||||
{isValueChanged && (
|
||||
<div className="absolute bottom-[-25px] text-sm right-0 break-words">
|
||||
<span
|
||||
className="line-through"
|
||||
style={{
|
||||
color: 'rgba(198, 34, 34, 1)',
|
||||
backgroundColor: 'rgba(254, 213, 213, 1)',
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(defaultValue)}
|
||||
</span>{' '}
|
||||
=>{' '}
|
||||
<span
|
||||
style={{
|
||||
color: 'rgba(20, 147, 67, 1)',
|
||||
backgroundColor: 'rgba(225, 250, 177, 1)',
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(value)}
|
||||
</span>
|
||||
{error && (
|
||||
<div className="absolute bottom-[-25px] text-sm right-0 break-words text-red-500">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -34,9 +34,7 @@ type ConfigGroup<T extends AppConfigModule> = {
|
||||
appConfig: AppConfig;
|
||||
}>[];
|
||||
};
|
||||
const IGNORED_MODULES: (keyof AppConfig)[] = [
|
||||
'copilot', // not ready
|
||||
];
|
||||
const IGNORED_MODULES: (keyof AppConfig)[] = [];
|
||||
|
||||
if (environment.isSelfHosted) {
|
||||
IGNORED_MODULES.push('payment');
|
||||
@@ -139,6 +137,39 @@ export const KNOWN_CONFIG_GROUPS = [
|
||||
module: 'oauth',
|
||||
fields: ['providers.google', 'providers.github', 'providers.oidc'],
|
||||
} as ConfigGroup<'oauth'>,
|
||||
{
|
||||
name: 'AI',
|
||||
module: 'copilot',
|
||||
fields: [
|
||||
'enabled',
|
||||
'providers.openai',
|
||||
'providers.gemini',
|
||||
'providers.perplexity',
|
||||
'providers.anthropic',
|
||||
'providers.fal',
|
||||
'unsplash',
|
||||
'exa',
|
||||
{
|
||||
key: 'storage',
|
||||
desc: 'The storage provider for copilot blobs',
|
||||
sub: 'provider',
|
||||
type: 'Enum',
|
||||
options: ['fs', 'aws-s3', 'cloudflare-r2'],
|
||||
},
|
||||
{
|
||||
key: 'storage',
|
||||
sub: 'bucket',
|
||||
type: 'String',
|
||||
desc: 'The bucket name for copilot blobs storage',
|
||||
},
|
||||
{
|
||||
key: 'storage',
|
||||
sub: 'config',
|
||||
type: 'JSON',
|
||||
desc: 'The config passed directly to the storage provider(e.g. aws-sdk)',
|
||||
},
|
||||
],
|
||||
} as ConfigGroup<'copilot'>,
|
||||
];
|
||||
|
||||
export const UNKNOWN_CONFIG_GROUPS = ALL_CONFIGURABLE_MODULES.filter(
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Button } from '@affine/admin/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@affine/admin/components/ui/dialog';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { AppConfig } from './config';
|
||||
|
||||
export const ConfirmChanges = ({
|
||||
updates,
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: {
|
||||
updates: AppConfig;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: () => void;
|
||||
}) => {
|
||||
const onClose = useCallback(() => {
|
||||
onOpenChange(false);
|
||||
}, [onOpenChange]);
|
||||
|
||||
const modifiedKeys = Object.keys(updates).filter(
|
||||
key => updates[key].from !== updates[key].to
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:w-[460px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="leading-7">
|
||||
Save Runtime Configurations ?
|
||||
</DialogTitle>
|
||||
<DialogDescription className="leading-6">
|
||||
Are you sure you want to save the following changes?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{modifiedKeys.length > 0 ? (
|
||||
<pre className="flex flex-col text-sm bg-zinc-100 gap-1 min-h-[64px] rounded-md p-[12px_16px_16px_12px] mt-2 overflow-auto">
|
||||
<p>{'{'}</p>
|
||||
{modifiedKeys.map(key => (
|
||||
<p key={key}>
|
||||
{' '} {key}:{' '}
|
||||
<span
|
||||
className="mr-2 line-through "
|
||||
style={{
|
||||
color: 'rgba(198, 34, 34, 1)',
|
||||
backgroundColor: 'rgba(254, 213, 213, 1)',
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(updates[key].from)}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: 'rgba(20, 147, 67, 1)',
|
||||
backgroundColor: 'rgba(225, 250, 177, 1)',
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(updates[key].to)}
|
||||
</span>
|
||||
,
|
||||
</p>
|
||||
))}
|
||||
<p>{'}'}</p>
|
||||
</pre>
|
||||
) : (
|
||||
'There is no change.'
|
||||
)}
|
||||
<DialogFooter className="mt-6">
|
||||
<div className="flex justify-end items-center w-full gap-2">
|
||||
<Button type="button" onClick={onClose} variant="outline">
|
||||
<span>Cancel</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
disabled={modifiedKeys.length === 0}
|
||||
>
|
||||
<span>Save</span>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { Button } from '@affine/admin/components/ui/button';
|
||||
import { ScrollArea } from '@affine/admin/components/ui/scroll-area';
|
||||
import { get } from 'lodash-es';
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { Header } from '../header';
|
||||
import { useNav } from '../nav/context';
|
||||
@@ -12,14 +12,10 @@ import {
|
||||
type AppConfig,
|
||||
} from './config';
|
||||
import { type ConfigInputProps, ConfigRow } from './config-input-row';
|
||||
import { ConfirmChanges } from './confirm-changes';
|
||||
import { useAppConfig } from './use-app-config';
|
||||
|
||||
export function SettingsPage() {
|
||||
const { appConfig, update, save, patchedAppConfig, updates } = useAppConfig();
|
||||
const [open, setOpen] = useState(false);
|
||||
const onOpen = useCallback(() => setOpen(true), [setOpen]);
|
||||
|
||||
const disableSave = Object.keys(updates).length === 0;
|
||||
|
||||
const saveChanges = useCallback(() => {
|
||||
@@ -27,7 +23,6 @@ export function SettingsPage() {
|
||||
return;
|
||||
}
|
||||
save();
|
||||
setOpen(false);
|
||||
}, [save, disableSave]);
|
||||
|
||||
return (
|
||||
@@ -40,7 +35,7 @@ export function SettingsPage() {
|
||||
size="icon"
|
||||
className="w-7 h-7"
|
||||
variant="ghost"
|
||||
onClick={onOpen}
|
||||
onClick={saveChanges}
|
||||
disabled={disableSave}
|
||||
>
|
||||
<CheckIcon size={20} />
|
||||
@@ -52,12 +47,6 @@ export function SettingsPage() {
|
||||
appConfig={appConfig}
|
||||
patchedAppConfig={patchedAppConfig}
|
||||
/>
|
||||
<ConfirmChanges
|
||||
updates={updates}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onConfirm={saveChanges}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -94,17 +83,18 @@ const AdminPanel = ({
|
||||
const descriptor = ALL_CONFIG_DESCRIPTORS[module][field];
|
||||
desc = descriptor.desc;
|
||||
props = {
|
||||
type: descriptor.type,
|
||||
defaultValue: get(appConfig[module], field),
|
||||
field: `${module}/${field}`,
|
||||
desc,
|
||||
onChange: onUpdate,
|
||||
type: descriptor.type,
|
||||
options: [],
|
||||
defaultValue: get(appConfig[module], field),
|
||||
onChange: onUpdate,
|
||||
};
|
||||
} else {
|
||||
const descriptor = ALL_CONFIG_DESCRIPTORS[module][field.key];
|
||||
|
||||
props = {
|
||||
field: `${module}/${field.key}${field.sub ? `/${field.sub}` : ''}`,
|
||||
desc: field.desc ?? descriptor.desc,
|
||||
type: field.type ?? descriptor.type,
|
||||
// @ts-expect-error for enum type
|
||||
@@ -113,7 +103,6 @@ const AdminPanel = ({
|
||||
appConfig[module],
|
||||
field.key + (field.sub ? '.' + field.sub : '')
|
||||
),
|
||||
field: `${module}/${field.key}${field.sub ? `/${field.sub}` : ''}`,
|
||||
onChange: onUpdate,
|
||||
};
|
||||
}
|
||||
@@ -128,5 +117,3 @@ const AdminPanel = ({
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
export { SettingsPage as Component };
|
||||
|
||||
@@ -25,7 +25,7 @@ export const useAppConfig = () => {
|
||||
query: appConfigQuery,
|
||||
});
|
||||
|
||||
const { trigger } = useMutation({
|
||||
const { trigger: saveUpdates } = useMutation({
|
||||
mutation: updateAppConfigMutation,
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ export const useAppConfig = () => {
|
||||
);
|
||||
|
||||
try {
|
||||
const savedUpdates = await trigger({
|
||||
const savedUpdates = await saveUpdates({
|
||||
updates: updateInputs,
|
||||
});
|
||||
await mutate(prev => {
|
||||
@@ -69,7 +69,7 @@ export const useAppConfig = () => {
|
||||
});
|
||||
console.error(e);
|
||||
}
|
||||
}, [updates, mutate, trigger]);
|
||||
}, [updates, mutate, saveUpdates]);
|
||||
|
||||
const update = useCallback(
|
||||
(path: string, value: any) => {
|
||||
|
||||
Reference in New Issue
Block a user