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:
forehalo
2025-05-27 06:07:26 +00:00
parent eed95366c9
commit 2f139bd02c
19 changed files with 291 additions and 185 deletions

View File

@@ -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>{' '}
=&gt;{' '}
<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>

View File

@@ -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(

View File

@@ -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>
);
};

View File

@@ -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 };

View File

@@ -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) => {