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:
@@ -1,6 +1,7 @@
|
|||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
|
|
||||||
import { createModule } from '../../../__tests__/create-module';
|
import { createModule } from '../../../__tests__/create-module';
|
||||||
|
import { InvalidAppConfig } from '../../error';
|
||||||
import { ConfigFactory, ConfigModule } from '..';
|
import { ConfigFactory, ConfigModule } from '..';
|
||||||
import { Config } from '../config';
|
import { Config } from '../config';
|
||||||
import { override } from '../register';
|
import { override } from '../register';
|
||||||
@@ -64,30 +65,29 @@ test('should override config', async t => {
|
|||||||
test('should validate config', t => {
|
test('should validate config', t => {
|
||||||
const config = module.get(ConfigFactory);
|
const config = module.get(ConfigFactory);
|
||||||
|
|
||||||
t.notThrows(() =>
|
t.is(
|
||||||
config.validate([
|
config.validate([
|
||||||
{
|
{
|
||||||
module: 'auth',
|
module: 'auth',
|
||||||
key: 'passwordRequirements',
|
key: 'passwordRequirements',
|
||||||
value: { max: 10, min: 6 },
|
value: { max: 10, min: 6 },
|
||||||
},
|
},
|
||||||
])
|
]),
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
t.throws(
|
const [error] = config.validate([
|
||||||
() =>
|
|
||||||
config.validate([
|
|
||||||
{
|
|
||||||
module: 'auth',
|
|
||||||
key: 'passwordRequirements',
|
|
||||||
value: { max: 10, min: 10 },
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
{
|
{
|
||||||
message: `Invalid config for module [auth] with key [passwordRequirements]
|
module: 'auth',
|
||||||
Value: {"max":10,"min":10}
|
key: 'passwordRequirements',
|
||||||
Error: Minimum length of password must be less than maximum length`,
|
value: { max: 10, min: 10 },
|
||||||
}
|
},
|
||||||
|
])!;
|
||||||
|
|
||||||
|
t.true(error instanceof InvalidAppConfig);
|
||||||
|
t.is(
|
||||||
|
error.message,
|
||||||
|
'Invalid app config for module `auth` with key `passwordRequirements`. Minimum length of password must be less than maximum length.'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,13 +33,17 @@ export class ConfigFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate(updates: Array<{ module: string; key: string; value: any }>) {
|
validate(updates: Array<{ module: string; key: string; value: any }>) {
|
||||||
const errors: string[] = [];
|
const errors: InvalidAppConfig[] = [];
|
||||||
|
|
||||||
updates.forEach(update => {
|
updates.forEach(update => {
|
||||||
const descriptor = APP_CONFIG_DESCRIPTORS[update.module]?.[update.key];
|
const descriptor = APP_CONFIG_DESCRIPTORS[update.module]?.[update.key];
|
||||||
if (!descriptor) {
|
if (!descriptor) {
|
||||||
errors.push(
|
errors.push(
|
||||||
`Invalid config for module [${update.module}] with unknown key [${update.key}]`
|
new InvalidAppConfig({
|
||||||
|
module: update.module,
|
||||||
|
key: update.key,
|
||||||
|
hint: `Unknown config [${update.key}]`,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -47,16 +51,18 @@ export class ConfigFactory {
|
|||||||
const { success, error } = descriptor.validate(update.value);
|
const { success, error } = descriptor.validate(update.value);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
error.issues.forEach(issue => {
|
error.issues.forEach(issue => {
|
||||||
errors.push(`Invalid config for module [${update.module}] with key [${update.key}]
|
errors.push(
|
||||||
Value: ${JSON.stringify(update.value)}
|
new InvalidAppConfig({
|
||||||
Error: ${issue.message}`);
|
module: update.module,
|
||||||
|
key: update.key,
|
||||||
|
hint: issue.message,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (errors.length > 0) {
|
return errors.length > 0 ? errors : null;
|
||||||
throw new InvalidAppConfig(errors.join('\n'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadDefault() {
|
private loadDefault() {
|
||||||
|
|||||||
@@ -877,7 +877,14 @@ export const USER_FRIENDLY_ERRORS = {
|
|||||||
// app config
|
// app config
|
||||||
invalid_app_config: {
|
invalid_app_config: {
|
||||||
type: 'invalid_input',
|
type: 'invalid_input',
|
||||||
message: 'Invalid app config.',
|
args: { module: 'string', key: 'string', hint: 'string' },
|
||||||
|
message: ({ module, key, hint }) =>
|
||||||
|
`Invalid app config for module \`${module}\` with key \`${key}\`. ${hint}.`,
|
||||||
|
},
|
||||||
|
invalid_app_config_input: {
|
||||||
|
type: 'invalid_input',
|
||||||
|
args: { message: 'string' },
|
||||||
|
message: ({ message }) => `Invalid app config input: ${message}`,
|
||||||
},
|
},
|
||||||
|
|
||||||
// indexer errors
|
// indexer errors
|
||||||
|
|||||||
@@ -1012,10 +1012,26 @@ export class MentionUserOneselfDenied extends UserFriendlyError {
|
|||||||
super('action_forbidden', 'mention_user_oneself_denied', message);
|
super('action_forbidden', 'mention_user_oneself_denied', message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ObjectType()
|
||||||
|
class InvalidAppConfigDataType {
|
||||||
|
@Field() module!: string
|
||||||
|
@Field() key!: string
|
||||||
|
@Field() hint!: string
|
||||||
|
}
|
||||||
|
|
||||||
export class InvalidAppConfig extends UserFriendlyError {
|
export class InvalidAppConfig extends UserFriendlyError {
|
||||||
constructor(message?: string) {
|
constructor(args: InvalidAppConfigDataType, message?: string | ((args: InvalidAppConfigDataType) => string)) {
|
||||||
super('invalid_input', 'invalid_app_config', message);
|
super('invalid_input', 'invalid_app_config', message, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ObjectType()
|
||||||
|
class InvalidAppConfigInputDataType {
|
||||||
|
@Field() message!: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidAppConfigInput extends UserFriendlyError {
|
||||||
|
constructor(args: InvalidAppConfigInputDataType, message?: string | ((args: InvalidAppConfigInputDataType) => string)) {
|
||||||
|
super('invalid_input', 'invalid_app_config_input', message, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1176,6 +1192,7 @@ export enum ErrorNames {
|
|||||||
MENTION_USER_DOC_ACCESS_DENIED,
|
MENTION_USER_DOC_ACCESS_DENIED,
|
||||||
MENTION_USER_ONESELF_DENIED,
|
MENTION_USER_ONESELF_DENIED,
|
||||||
INVALID_APP_CONFIG,
|
INVALID_APP_CONFIG,
|
||||||
|
INVALID_APP_CONFIG_INPUT,
|
||||||
SEARCH_PROVIDER_NOT_FOUND,
|
SEARCH_PROVIDER_NOT_FOUND,
|
||||||
INVALID_SEARCH_PROVIDER_REQUEST,
|
INVALID_SEARCH_PROVIDER_REQUEST,
|
||||||
INVALID_INDEXER_INPUT
|
INVALID_INDEXER_INPUT
|
||||||
@@ -1187,5 +1204,5 @@ registerEnumType(ErrorNames, {
|
|||||||
export const ErrorDataUnionType = createUnionType({
|
export const ErrorDataUnionType = createUnionType({
|
||||||
name: 'ErrorDataUnion',
|
name: 'ErrorDataUnion',
|
||||||
types: () =>
|
types: () =>
|
||||||
[GraphqlBadRequestDataType, HttpRequestErrorDataType, QueryTooLongDataType, ValidationErrorDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidOauthResponseDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, NoMoreSeatDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderNotSupportedDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, CopilotFailedToMatchGlobalContextDataType, CopilotFailedToAddWorkspaceFileEmbeddingDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseToActivateDataType, InvalidLicenseUpdateParamsDataType, UnsupportedClientVersionDataType, MentionUserDocAccessDeniedDataType, InvalidSearchProviderRequestDataType, InvalidIndexerInputDataType] as const,
|
[GraphqlBadRequestDataType, HttpRequestErrorDataType, QueryTooLongDataType, ValidationErrorDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, InvalidOauthCallbackCodeDataType, MissingOauthQueryParameterDataType, InvalidOauthResponseDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocActionDeniedDataType, DocUpdateBlockedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, NoMoreSeatDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotDocNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderNotSupportedDataType, CopilotProviderSideErrorDataType, CopilotInvalidContextDataType, CopilotContextFileNotSupportedDataType, CopilotFailedToModifyContextDataType, CopilotFailedToMatchContextDataType, CopilotFailedToMatchGlobalContextDataType, CopilotFailedToAddWorkspaceFileEmbeddingDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseToActivateDataType, InvalidLicenseUpdateParamsDataType, UnsupportedClientVersionDataType, MentionUserDocAccessDeniedDataType, InvalidAppConfigDataType, InvalidAppConfigInputDataType, InvalidSearchProviderRequestDataType, InvalidIndexerInputDataType] as const,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Sinon from 'sinon';
|
|||||||
|
|
||||||
import { createModule } from '../../../__tests__/create-module';
|
import { createModule } from '../../../__tests__/create-module';
|
||||||
import { Mockers } from '../../../__tests__/mocks';
|
import { Mockers } from '../../../__tests__/mocks';
|
||||||
|
import { InvalidAppConfigInput } from '../../../base';
|
||||||
import { Models } from '../../../models';
|
import { Models } from '../../../models';
|
||||||
import { ServerService } from '../service';
|
import { ServerService } from '../service';
|
||||||
|
|
||||||
@@ -47,9 +48,7 @@ test('should validate config before update', async t => {
|
|||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
message: `Invalid config for module [server] with key [externalUrl]
|
instanceOf: InvalidAppConfigInput,
|
||||||
Value: "invalid-url@some-domain.com"
|
|
||||||
Error: Invalid url`,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ Error: Invalid url`,
|
|||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
message: `Invalid config for module [auth] with unknown key [unknown-key]`,
|
instanceOf: InvalidAppConfigInput,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,24 @@ class UpdateAppConfigInput {
|
|||||||
value!: any;
|
value!: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
class AppConfigValidateResult {
|
||||||
|
@Field()
|
||||||
|
module!: string;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
key!: string;
|
||||||
|
|
||||||
|
@Field(() => GraphQLJSON)
|
||||||
|
value!: any;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
valid!: boolean;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Admin()
|
@Admin()
|
||||||
@Resolver(() => GraphQLJSONObject)
|
@Resolver(() => GraphQLJSONObject)
|
||||||
export class AppConfigResolver {
|
export class AppConfigResolver {
|
||||||
@@ -204,4 +222,28 @@ export class AppConfigResolver {
|
|||||||
): Promise<DeepPartial<AppConfig>> {
|
): Promise<DeepPartial<AppConfig>> {
|
||||||
return await this.service.updateConfig(me.id, updates);
|
return await this.service.updateConfig(me.id, updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => [AppConfigValidateResult], {
|
||||||
|
description: 'validate app configuration',
|
||||||
|
})
|
||||||
|
async validateAppConfig(
|
||||||
|
@Args('updates', { type: () => [UpdateAppConfigInput] })
|
||||||
|
updates: UpdateAppConfigInput[]
|
||||||
|
): Promise<AppConfigValidateResult[]> {
|
||||||
|
const errors = this.service.validateConfig(updates);
|
||||||
|
|
||||||
|
return updates.map(update => {
|
||||||
|
const error = errors?.find(
|
||||||
|
error =>
|
||||||
|
error.data.module === update.module && error.data.key === update.key
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
module: update.module,
|
||||||
|
key: update.key,
|
||||||
|
value: update.value,
|
||||||
|
valid: !error,
|
||||||
|
error: error?.data.hint,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
||||||
import { set } from 'lodash-es';
|
import { set } from 'lodash-es';
|
||||||
|
|
||||||
import { ConfigFactory, EventBus, OnEvent } from '../../base';
|
import {
|
||||||
|
ConfigFactory,
|
||||||
|
EventBus,
|
||||||
|
InvalidAppConfigInput,
|
||||||
|
OnEvent,
|
||||||
|
} from '../../base';
|
||||||
import { Models } from '../../models';
|
import { Models } from '../../models';
|
||||||
import { ServerFeature } from './types';
|
import { ServerFeature } from './types';
|
||||||
|
|
||||||
@@ -60,11 +65,21 @@ export class ServerService implements OnApplicationBootstrap {
|
|||||||
return this.configFactory.clone();
|
return this.configFactory.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateConfig(updates: Array<{ module: string; key: string; value: any }>) {
|
||||||
|
return this.configFactory.validate(updates);
|
||||||
|
}
|
||||||
|
|
||||||
async updateConfig(
|
async updateConfig(
|
||||||
user: string,
|
user: string,
|
||||||
updates: Array<{ module: string; key: string; value: any }>
|
updates: Array<{ module: string; key: string; value: any }>
|
||||||
): Promise<DeepPartial<AppConfig>> {
|
): Promise<DeepPartial<AppConfig>> {
|
||||||
this.configFactory.validate(updates);
|
const errors = this.configFactory.validate(updates);
|
||||||
|
|
||||||
|
if (errors?.length) {
|
||||||
|
throw new InvalidAppConfigInput({
|
||||||
|
message: errors.map(error => error.message).join('\n'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const promises = await this.models.appConfig.save(
|
const promises = await this.models.appConfig.save(
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { resolve } from 'node:path';
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { Command, CommandRunner } from 'nest-commander';
|
import { Command, CommandRunner } from 'nest-commander';
|
||||||
|
|
||||||
import { ConfigFactory } from '../../base';
|
import { ConfigFactory, InvalidAppConfigInput } from '../../base';
|
||||||
import { Models } from '../../models';
|
import { Models } from '../../models';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
@@ -50,7 +50,13 @@ export class ImportConfigCommand extends CommandRunner {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.configFactory.validate(forValidation);
|
const errors = this.configFactory.validate(forValidation);
|
||||||
|
|
||||||
|
if (errors?.length) {
|
||||||
|
throw new InvalidAppConfigInput({
|
||||||
|
message: errors.map(error => error.message).join('\n '),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-expect-error null as user id
|
// @ts-expect-error null as user id
|
||||||
await this.models.appConfig.save(null, forSaving);
|
await this.models.appConfig.save(null, forSaving);
|
||||||
|
|||||||
@@ -71,6 +71,14 @@ type AlreadyInSpaceDataType {
|
|||||||
spaceId: String!
|
spaceId: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppConfigValidateResult {
|
||||||
|
error: String
|
||||||
|
key: String!
|
||||||
|
module: String!
|
||||||
|
valid: Boolean!
|
||||||
|
value: JSON!
|
||||||
|
}
|
||||||
|
|
||||||
type BlobNotFoundDataType {
|
type BlobNotFoundDataType {
|
||||||
blobId: String!
|
blobId: String!
|
||||||
spaceId: String!
|
spaceId: String!
|
||||||
@@ -523,7 +531,7 @@ type EditorType {
|
|||||||
name: String!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToAddWorkspaceFileEmbeddingDataType | CopilotFailedToMatchContextDataType | CopilotFailedToMatchGlobalContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderNotSupportedDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | DocUpdateBlockedDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | HttpRequestErrorDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidIndexerInputDataType | InvalidLicenseToActivateDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidOauthResponseDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | InvalidSearchProviderRequestDataType | MemberNotFoundInSpaceDataType | MentionUserDocAccessDeniedDataType | MissingOauthQueryParameterDataType | NoMoreSeatDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | ValidationErrorDataType | VersionRejectedDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType
|
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotContextFileNotSupportedDataType | CopilotDocNotFoundDataType | CopilotFailedToAddWorkspaceFileEmbeddingDataType | CopilotFailedToMatchContextDataType | CopilotFailedToMatchGlobalContextDataType | CopilotFailedToModifyContextDataType | CopilotInvalidContextDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderNotSupportedDataType | CopilotProviderSideErrorDataType | DocActionDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | DocUpdateBlockedDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | GraphqlBadRequestDataType | HttpRequestErrorDataType | InvalidAppConfigDataType | InvalidAppConfigInputDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidIndexerInputDataType | InvalidLicenseToActivateDataType | InvalidLicenseUpdateParamsDataType | InvalidOauthCallbackCodeDataType | InvalidOauthResponseDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | InvalidSearchProviderRequestDataType | MemberNotFoundInSpaceDataType | MentionUserDocAccessDeniedDataType | MissingOauthQueryParameterDataType | NoMoreSeatDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedClientVersionDataType | UnsupportedSubscriptionPlanDataType | ValidationErrorDataType | VersionRejectedDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType
|
||||||
|
|
||||||
enum ErrorNames {
|
enum ErrorNames {
|
||||||
ACCESS_DENIED
|
ACCESS_DENIED
|
||||||
@@ -589,6 +597,7 @@ enum ErrorNames {
|
|||||||
HTTP_REQUEST_ERROR
|
HTTP_REQUEST_ERROR
|
||||||
INTERNAL_SERVER_ERROR
|
INTERNAL_SERVER_ERROR
|
||||||
INVALID_APP_CONFIG
|
INVALID_APP_CONFIG
|
||||||
|
INVALID_APP_CONFIG_INPUT
|
||||||
INVALID_AUTH_STATE
|
INVALID_AUTH_STATE
|
||||||
INVALID_CHECKOUT_PARAMETERS
|
INVALID_CHECKOUT_PARAMETERS
|
||||||
INVALID_EMAIL
|
INVALID_EMAIL
|
||||||
@@ -729,6 +738,16 @@ input ImportUsersInput {
|
|||||||
users: [CreateUserInput!]!
|
users: [CreateUserInput!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InvalidAppConfigDataType {
|
||||||
|
hint: String!
|
||||||
|
key: String!
|
||||||
|
module: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvalidAppConfigInputDataType {
|
||||||
|
message: String!
|
||||||
|
}
|
||||||
|
|
||||||
type InvalidEmailDataType {
|
type InvalidEmailDataType {
|
||||||
email: String!
|
email: String!
|
||||||
}
|
}
|
||||||
@@ -1198,6 +1217,9 @@ type Mutation {
|
|||||||
|
|
||||||
"""Upload user avatar"""
|
"""Upload user avatar"""
|
||||||
uploadAvatar(avatar: Upload!): UserType!
|
uploadAvatar(avatar: Upload!): UserType!
|
||||||
|
|
||||||
|
"""validate app configuration"""
|
||||||
|
validateAppConfig(updates: [UpdateAppConfigInput!]!): [AppConfigValidateResult!]!
|
||||||
verifyEmail(token: String!): Boolean!
|
verifyEmail(token: String!): Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
mutation validateConfig($updates: [UpdateAppConfigInput!]!) {
|
||||||
|
validateAppConfig(updates: $updates) {
|
||||||
|
module
|
||||||
|
key
|
||||||
|
value
|
||||||
|
valid
|
||||||
|
error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -246,6 +246,20 @@ export const updateAppConfigMutation = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const validateConfigMutation = {
|
||||||
|
id: 'validateConfigMutation' as const,
|
||||||
|
op: 'validateConfig',
|
||||||
|
query: `mutation validateConfig($updates: [UpdateAppConfigInput!]!) {
|
||||||
|
validateAppConfig(updates: $updates) {
|
||||||
|
module
|
||||||
|
key
|
||||||
|
value
|
||||||
|
valid
|
||||||
|
error
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteBlobMutation = {
|
export const deleteBlobMutation = {
|
||||||
id: 'deleteBlobMutation' as const,
|
id: 'deleteBlobMutation' as const,
|
||||||
op: 'deleteBlob',
|
op: 'deleteBlob',
|
||||||
|
|||||||
@@ -109,6 +109,15 @@ export interface AlreadyInSpaceDataType {
|
|||||||
spaceId: Scalars['String']['output'];
|
spaceId: Scalars['String']['output'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppConfigValidateResult {
|
||||||
|
__typename?: 'AppConfigValidateResult';
|
||||||
|
error: Maybe<Scalars['String']['output']>;
|
||||||
|
key: Scalars['String']['output'];
|
||||||
|
module: Scalars['String']['output'];
|
||||||
|
valid: Scalars['Boolean']['output'];
|
||||||
|
value: Scalars['JSON']['output'];
|
||||||
|
}
|
||||||
|
|
||||||
export interface BlobNotFoundDataType {
|
export interface BlobNotFoundDataType {
|
||||||
__typename?: 'BlobNotFoundDataType';
|
__typename?: 'BlobNotFoundDataType';
|
||||||
blobId: Scalars['String']['output'];
|
blobId: Scalars['String']['output'];
|
||||||
@@ -665,6 +674,8 @@ export type ErrorDataUnion =
|
|||||||
| ExpectToUpdateDocUserRoleDataType
|
| ExpectToUpdateDocUserRoleDataType
|
||||||
| GraphqlBadRequestDataType
|
| GraphqlBadRequestDataType
|
||||||
| HttpRequestErrorDataType
|
| HttpRequestErrorDataType
|
||||||
|
| InvalidAppConfigDataType
|
||||||
|
| InvalidAppConfigInputDataType
|
||||||
| InvalidEmailDataType
|
| InvalidEmailDataType
|
||||||
| InvalidHistoryTimestampDataType
|
| InvalidHistoryTimestampDataType
|
||||||
| InvalidIndexerInputDataType
|
| InvalidIndexerInputDataType
|
||||||
@@ -762,6 +773,7 @@ export enum ErrorNames {
|
|||||||
HTTP_REQUEST_ERROR = 'HTTP_REQUEST_ERROR',
|
HTTP_REQUEST_ERROR = 'HTTP_REQUEST_ERROR',
|
||||||
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
|
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
|
||||||
INVALID_APP_CONFIG = 'INVALID_APP_CONFIG',
|
INVALID_APP_CONFIG = 'INVALID_APP_CONFIG',
|
||||||
|
INVALID_APP_CONFIG_INPUT = 'INVALID_APP_CONFIG_INPUT',
|
||||||
INVALID_AUTH_STATE = 'INVALID_AUTH_STATE',
|
INVALID_AUTH_STATE = 'INVALID_AUTH_STATE',
|
||||||
INVALID_CHECKOUT_PARAMETERS = 'INVALID_CHECKOUT_PARAMETERS',
|
INVALID_CHECKOUT_PARAMETERS = 'INVALID_CHECKOUT_PARAMETERS',
|
||||||
INVALID_EMAIL = 'INVALID_EMAIL',
|
INVALID_EMAIL = 'INVALID_EMAIL',
|
||||||
@@ -906,6 +918,18 @@ export interface ImportUsersInput {
|
|||||||
users: Array<CreateUserInput>;
|
users: Array<CreateUserInput>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InvalidAppConfigDataType {
|
||||||
|
__typename?: 'InvalidAppConfigDataType';
|
||||||
|
hint: Scalars['String']['output'];
|
||||||
|
key: Scalars['String']['output'];
|
||||||
|
module: Scalars['String']['output'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvalidAppConfigInputDataType {
|
||||||
|
__typename?: 'InvalidAppConfigInputDataType';
|
||||||
|
message: Scalars['String']['output'];
|
||||||
|
}
|
||||||
|
|
||||||
export interface InvalidEmailDataType {
|
export interface InvalidEmailDataType {
|
||||||
__typename?: 'InvalidEmailDataType';
|
__typename?: 'InvalidEmailDataType';
|
||||||
email: Scalars['String']['output'];
|
email: Scalars['String']['output'];
|
||||||
@@ -1334,6 +1358,8 @@ export interface Mutation {
|
|||||||
updateWorkspaceEmbeddingIgnoredDocs: Scalars['Int']['output'];
|
updateWorkspaceEmbeddingIgnoredDocs: Scalars['Int']['output'];
|
||||||
/** Upload user avatar */
|
/** Upload user avatar */
|
||||||
uploadAvatar: UserType;
|
uploadAvatar: UserType;
|
||||||
|
/** validate app configuration */
|
||||||
|
validateAppConfig: Array<AppConfigValidateResult>;
|
||||||
verifyEmail: Scalars['Boolean']['output'];
|
verifyEmail: Scalars['Boolean']['output'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1712,6 +1738,10 @@ export interface MutationUploadAvatarArgs {
|
|||||||
avatar: Scalars['Upload']['input'];
|
avatar: Scalars['Upload']['input'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MutationValidateAppConfigArgs {
|
||||||
|
updates: Array<UpdateAppConfigInput>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MutationVerifyEmailArgs {
|
export interface MutationVerifyEmailArgs {
|
||||||
token: Scalars['String']['input'];
|
token: Scalars['String']['input'];
|
||||||
}
|
}
|
||||||
@@ -2900,6 +2930,22 @@ export type UpdateAppConfigMutation = {
|
|||||||
updateAppConfig: any;
|
updateAppConfig: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ValidateConfigMutationVariables = Exact<{
|
||||||
|
updates: Array<UpdateAppConfigInput> | UpdateAppConfigInput;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type ValidateConfigMutation = {
|
||||||
|
__typename?: 'Mutation';
|
||||||
|
validateAppConfig: Array<{
|
||||||
|
__typename?: 'AppConfigValidateResult';
|
||||||
|
module: string;
|
||||||
|
key: string;
|
||||||
|
value: Record<string, string>;
|
||||||
|
valid: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
export type DeleteBlobMutationVariables = Exact<{
|
export type DeleteBlobMutationVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
key: Scalars['String']['input'];
|
key: Scalars['String']['input'];
|
||||||
@@ -5312,6 +5358,11 @@ export type Mutations =
|
|||||||
variables: UpdateAppConfigMutationVariables;
|
variables: UpdateAppConfigMutationVariables;
|
||||||
response: UpdateAppConfigMutation;
|
response: UpdateAppConfigMutation;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'validateConfigMutation';
|
||||||
|
variables: ValidateConfigMutationVariables;
|
||||||
|
response: ValidateConfigMutation;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'deleteBlobMutation';
|
name: 'deleteBlobMutation';
|
||||||
variables: DeleteBlobMutationVariables;
|
variables: DeleteBlobMutationVariables;
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@affine/admin/components/ui/select';
|
} from '@affine/admin/components/ui/select';
|
||||||
import { Switch } from '@affine/admin/components/ui/switch';
|
import { Switch } from '@affine/admin/components/ui/switch';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { Textarea } from '../../components/ui/textarea';
|
import { Textarea } from '../../components/ui/textarea';
|
||||||
import { isEqual } from './utils';
|
|
||||||
|
|
||||||
export type ConfigInputProps = {
|
export type ConfigInputProps = {
|
||||||
field: string;
|
field: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
onChange: (key: string, value: any) => void;
|
onChange: (field: string, value: any) => void;
|
||||||
|
error?: string;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
type: 'String' | 'Number' | 'Boolean' | 'JSON';
|
type: 'String' | 'Number' | 'Boolean' | 'JSON';
|
||||||
@@ -33,6 +33,7 @@ const Inputs: Record<
|
|||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
onChange: (value?: any) => void;
|
onChange: (value?: any) => void;
|
||||||
options?: string[];
|
options?: string[];
|
||||||
|
error?: string;
|
||||||
}>
|
}>
|
||||||
> = {
|
> = {
|
||||||
Boolean: function SwitchInput({ defaultValue, onChange }) {
|
Boolean: function SwitchInput({ defaultValue, onChange }) {
|
||||||
@@ -114,22 +115,18 @@ export const ConfigRow = ({
|
|||||||
type,
|
type,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
onChange,
|
onChange,
|
||||||
|
error,
|
||||||
...props
|
...props
|
||||||
}: ConfigInputProps) => {
|
}: ConfigInputProps) => {
|
||||||
const [value, setValue] = useState(defaultValue);
|
const Input = Inputs[type] ?? Inputs.JSON;
|
||||||
|
|
||||||
const isValueChanged = !isEqual(value, defaultValue);
|
|
||||||
|
|
||||||
const onValueChange = useCallback(
|
const onValueChange = useCallback(
|
||||||
(value?: any) => {
|
(value?: any) => {
|
||||||
onChange(field, value);
|
onChange(field, value);
|
||||||
setValue(value);
|
|
||||||
},
|
},
|
||||||
[field, onChange]
|
[field, onChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
const Input = Inputs[type] ?? Inputs.JSON;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex justify-between flex-grow space-y-[10px]
|
className={`flex justify-between flex-grow space-y-[10px]
|
||||||
@@ -140,28 +137,12 @@ export const ConfigRow = ({
|
|||||||
<Input
|
<Input
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
onChange={onValueChange}
|
onChange={onValueChange}
|
||||||
|
error={error}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{isValueChanged && (
|
{error && (
|
||||||
<div className="absolute bottom-[-25px] text-sm right-0 break-words">
|
<div className="absolute bottom-[-25px] text-sm right-0 break-words text-red-500">
|
||||||
<span
|
{error}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ type ConfigGroup<T extends AppConfigModule> = {
|
|||||||
appConfig: AppConfig;
|
appConfig: AppConfig;
|
||||||
}>[];
|
}>[];
|
||||||
};
|
};
|
||||||
const IGNORED_MODULES: (keyof AppConfig)[] = [
|
const IGNORED_MODULES: (keyof AppConfig)[] = [];
|
||||||
'copilot', // not ready
|
|
||||||
];
|
|
||||||
|
|
||||||
if (environment.isSelfHosted) {
|
if (environment.isSelfHosted) {
|
||||||
IGNORED_MODULES.push('payment');
|
IGNORED_MODULES.push('payment');
|
||||||
@@ -139,6 +137,39 @@ export const KNOWN_CONFIG_GROUPS = [
|
|||||||
module: 'oauth',
|
module: 'oauth',
|
||||||
fields: ['providers.google', 'providers.github', 'providers.oidc'],
|
fields: ['providers.google', 'providers.github', 'providers.oidc'],
|
||||||
} as ConfigGroup<'oauth'>,
|
} 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(
|
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 { ScrollArea } from '@affine/admin/components/ui/scroll-area';
|
||||||
import { get } from 'lodash-es';
|
import { get } from 'lodash-es';
|
||||||
import { CheckIcon } from 'lucide-react';
|
import { CheckIcon } from 'lucide-react';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { Header } from '../header';
|
import { Header } from '../header';
|
||||||
import { useNav } from '../nav/context';
|
import { useNav } from '../nav/context';
|
||||||
@@ -12,14 +12,10 @@ import {
|
|||||||
type AppConfig,
|
type AppConfig,
|
||||||
} from './config';
|
} from './config';
|
||||||
import { type ConfigInputProps, ConfigRow } from './config-input-row';
|
import { type ConfigInputProps, ConfigRow } from './config-input-row';
|
||||||
import { ConfirmChanges } from './confirm-changes';
|
|
||||||
import { useAppConfig } from './use-app-config';
|
import { useAppConfig } from './use-app-config';
|
||||||
|
|
||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
const { appConfig, update, save, patchedAppConfig, updates } = useAppConfig();
|
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 disableSave = Object.keys(updates).length === 0;
|
||||||
|
|
||||||
const saveChanges = useCallback(() => {
|
const saveChanges = useCallback(() => {
|
||||||
@@ -27,7 +23,6 @@ export function SettingsPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
save();
|
save();
|
||||||
setOpen(false);
|
|
||||||
}, [save, disableSave]);
|
}, [save, disableSave]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -40,7 +35,7 @@ export function SettingsPage() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="w-7 h-7"
|
className="w-7 h-7"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={onOpen}
|
onClick={saveChanges}
|
||||||
disabled={disableSave}
|
disabled={disableSave}
|
||||||
>
|
>
|
||||||
<CheckIcon size={20} />
|
<CheckIcon size={20} />
|
||||||
@@ -52,12 +47,6 @@ export function SettingsPage() {
|
|||||||
appConfig={appConfig}
|
appConfig={appConfig}
|
||||||
patchedAppConfig={patchedAppConfig}
|
patchedAppConfig={patchedAppConfig}
|
||||||
/>
|
/>
|
||||||
<ConfirmChanges
|
|
||||||
updates={updates}
|
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
onConfirm={saveChanges}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -94,17 +83,18 @@ const AdminPanel = ({
|
|||||||
const descriptor = ALL_CONFIG_DESCRIPTORS[module][field];
|
const descriptor = ALL_CONFIG_DESCRIPTORS[module][field];
|
||||||
desc = descriptor.desc;
|
desc = descriptor.desc;
|
||||||
props = {
|
props = {
|
||||||
type: descriptor.type,
|
|
||||||
defaultValue: get(appConfig[module], field),
|
|
||||||
field: `${module}/${field}`,
|
field: `${module}/${field}`,
|
||||||
desc,
|
desc,
|
||||||
onChange: onUpdate,
|
type: descriptor.type,
|
||||||
options: [],
|
options: [],
|
||||||
|
defaultValue: get(appConfig[module], field),
|
||||||
|
onChange: onUpdate,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const descriptor = ALL_CONFIG_DESCRIPTORS[module][field.key];
|
const descriptor = ALL_CONFIG_DESCRIPTORS[module][field.key];
|
||||||
|
|
||||||
props = {
|
props = {
|
||||||
|
field: `${module}/${field.key}${field.sub ? `/${field.sub}` : ''}`,
|
||||||
desc: field.desc ?? descriptor.desc,
|
desc: field.desc ?? descriptor.desc,
|
||||||
type: field.type ?? descriptor.type,
|
type: field.type ?? descriptor.type,
|
||||||
// @ts-expect-error for enum type
|
// @ts-expect-error for enum type
|
||||||
@@ -113,7 +103,6 @@ const AdminPanel = ({
|
|||||||
appConfig[module],
|
appConfig[module],
|
||||||
field.key + (field.sub ? '.' + field.sub : '')
|
field.key + (field.sub ? '.' + field.sub : '')
|
||||||
),
|
),
|
||||||
field: `${module}/${field.key}${field.sub ? `/${field.sub}` : ''}`,
|
|
||||||
onChange: onUpdate,
|
onChange: onUpdate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -128,5 +117,3 @@ const AdminPanel = ({
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { SettingsPage as Component };
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const useAppConfig = () => {
|
|||||||
query: appConfigQuery,
|
query: appConfigQuery,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { trigger } = useMutation({
|
const { trigger: saveUpdates } = useMutation({
|
||||||
mutation: updateAppConfigMutation,
|
mutation: updateAppConfigMutation,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ export const useAppConfig = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedUpdates = await trigger({
|
const savedUpdates = await saveUpdates({
|
||||||
updates: updateInputs,
|
updates: updateInputs,
|
||||||
});
|
});
|
||||||
await mutate(prev => {
|
await mutate(prev => {
|
||||||
@@ -69,7 +69,7 @@ export const useAppConfig = () => {
|
|||||||
});
|
});
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}, [updates, mutate, trigger]);
|
}, [updates, mutate, saveUpdates]);
|
||||||
|
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(path: string, value: any) => {
|
(path: string, value: any) => {
|
||||||
|
|||||||
@@ -8812,9 +8812,19 @@ export function useAFFiNEI18N(): {
|
|||||||
*/
|
*/
|
||||||
["error.MENTION_USER_ONESELF_DENIED"](): string;
|
["error.MENTION_USER_ONESELF_DENIED"](): string;
|
||||||
/**
|
/**
|
||||||
* `Invalid app config.`
|
* `Invalid app config for module `{{module}}` with key `{{key}}`. {{hint}}.`
|
||||||
*/
|
*/
|
||||||
["error.INVALID_APP_CONFIG"](): string;
|
["error.INVALID_APP_CONFIG"](options: Readonly<{
|
||||||
|
module: string;
|
||||||
|
key: string;
|
||||||
|
hint: string;
|
||||||
|
}>): string;
|
||||||
|
/**
|
||||||
|
* `Invalid app config input: {{message}}`
|
||||||
|
*/
|
||||||
|
["error.INVALID_APP_CONFIG_INPUT"](options: {
|
||||||
|
readonly message: string;
|
||||||
|
}): string;
|
||||||
/**
|
/**
|
||||||
* `Search provider not found.`
|
* `Search provider not found.`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2176,7 +2176,8 @@
|
|||||||
"error.NOTIFICATION_NOT_FOUND": "Notification not found.",
|
"error.NOTIFICATION_NOT_FOUND": "Notification not found.",
|
||||||
"error.MENTION_USER_DOC_ACCESS_DENIED": "Mentioned user can not access doc {{docId}}.",
|
"error.MENTION_USER_DOC_ACCESS_DENIED": "Mentioned user can not access doc {{docId}}.",
|
||||||
"error.MENTION_USER_ONESELF_DENIED": "You can not mention yourself.",
|
"error.MENTION_USER_ONESELF_DENIED": "You can not mention yourself.",
|
||||||
"error.INVALID_APP_CONFIG": "Invalid app config.",
|
"error.INVALID_APP_CONFIG": "Invalid app config for module `{{module}}` with key `{{key}}`. {{hint}}.",
|
||||||
|
"error.INVALID_APP_CONFIG_INPUT": "Invalid app config input: {{message}}",
|
||||||
"error.SEARCH_PROVIDER_NOT_FOUND": "Search provider not found.",
|
"error.SEARCH_PROVIDER_NOT_FOUND": "Search provider not found.",
|
||||||
"error.INVALID_SEARCH_PROVIDER_REQUEST": "Invalid request argument to search provider: {{reason}}",
|
"error.INVALID_SEARCH_PROVIDER_REQUEST": "Invalid request argument to search provider: {{reason}}",
|
||||||
"error.INVALID_INDEXER_INPUT": "Invalid indexer input: {{reason}}"
|
"error.INVALID_INDEXER_INPUT": "Invalid indexer input: {{reason}}"
|
||||||
|
|||||||
Reference in New Issue
Block a user