mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 05:29:26 +08:00
feat: supports configuring dynamic environment variables and secrets (#5966)
* feat: environments plugin * feat: improve code * fix: improve code * feat: improve code * refactor: package description * feat: bulk import * fix: remove * refactor: file manager support environment variables * refactor: file manager support environment variables * refactor: map manager support environment variables * refactor: support environment variables * refactor: support environment variables * refactor: support delete environment variables * fix: bug * refactor: workflow support environment variables * refactor: email environment variables * refactor: support bulk import * refactor: support bulk import * refactor: support bulk import * refactor: support bulk import * refactor: code improve * feat: env * chore: update * feat: environment * fix: bug * fix: acl snippet * fix: acl snippets * chore: map manager * refactor: support line break * refactor: support password * chore: environment variables * fix: bug * fix: bug * chore: enviroment variables * chore: system settings * fix: improve code * feat: verification * feat: map * feat: file-manager * feat: notification * fix: bug * feat: workflow * fix: improve code * fix: bug * feat: data-source * feat: auth * fix: error * fix: bug * refactor: description * refactor: locale * refactor: locale * refactor: locale * refactor: code improve * refactor: locale * refactor: locale * style: style improve * fix: error * fix: bug * fix: bug * refactor: environment * fix: ellipsis * refactor: password * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * chore: test * fix: cache * fix: mysql dialect options * refactor: email config form * fix: bug * fix: bug * fix: authenticator.dataValues parse * fix: include undefined * fix: json * fix: json parse * chore: enviromentProvider * fix: acl * fix: rowKey * fix: update ProviderOptions.tsx * feat: get app instance * fix: bug * fix: text * fix: build error * fix: error * chore: migration rules options * chore: migration rules * refactor: code improve * feat: env v2 * chore: environment varibales * chore: environment serve * fix: getVariables * feat: improve code * fix: bug * chore: collection options for migration * chore: tree collection options * chore: migration rules * chore: migration rules * chore: env api * chore: env api * fix: optionsKeysNotAllowedInEnv * fix: required true * fix: improve code * fix: app refresh * fix: remove db.import * fix: type error * fix: map * refactor: locale improve * refactor: tx-cos * fix: undefined * refactor: code improve * chore: use bookworm * fix: npm add user * fix: npm login * fix: npm adduser * fix: npm adduser * fix: expect * fix: expect * fix: environmentVariables * refactor: support bulk delete & filter * refactor: locale improve * feat: filter * refactor: useGlobalVariable * fix: scope * fix: bug * fix: optionsKeysNotAllowedInEnv * fix: test error * fix: test * fix: test * feat: improve code --------- Co-authored-by: chenos <chenlinxh@gmail.com> Co-authored-by: Chareice <chareice@live.com>
This commit is contained in:
parent
cbdd5ffe8c
commit
5d5f455b3c
14
.gitignore
vendored
14
.gitignore
vendored
@ -20,27 +20,13 @@ docs-dist/
|
|||||||
dist/
|
dist/
|
||||||
docker/**/storage
|
docker/**/storage
|
||||||
cache/diskstore-*
|
cache/diskstore-*
|
||||||
*.nbdump
|
|
||||||
storage/duplicator/*
|
|
||||||
storage/backups/*
|
|
||||||
**/.dumi/tmp
|
**/.dumi/tmp
|
||||||
**/.dumi/tmp-test
|
**/.dumi/tmp-test
|
||||||
**/.dumi/tmp-production
|
**/.dumi/tmp-production
|
||||||
packages/core/client/docs/contributing.md
|
packages/core/client/docs/contributing.md
|
||||||
packages/core/app/client/src/.plugins
|
packages/core/app/client/src/.plugins
|
||||||
storage/plugins
|
|
||||||
storage/tar
|
|
||||||
storage/tmp
|
|
||||||
storage/print-templates
|
|
||||||
storage/cache
|
|
||||||
storage/app.watch.ts
|
|
||||||
storage/.upgrading
|
|
||||||
storage/logs-e2e
|
|
||||||
storage/uploads-e2e
|
|
||||||
storage/.pm2-*
|
|
||||||
tsconfig.paths.json
|
tsconfig.paths.json
|
||||||
/playwright
|
/playwright
|
||||||
/storage/playwright
|
|
||||||
.swc
|
.swc
|
||||||
ncc-cache/
|
ncc-cache/
|
||||||
yarn--**
|
yarn--**
|
||||||
|
17
Dockerfile
17
Dockerfile
@ -1,4 +1,4 @@
|
|||||||
FROM node:20.13-bullseye as builder
|
FROM node:20-bookworm as builder
|
||||||
ARG VERDACCIO_URL=http://host.docker.internal:10104/
|
ARG VERDACCIO_URL=http://host.docker.internal:10104/
|
||||||
ARG COMMIT_HASH
|
ARG COMMIT_HASH
|
||||||
ARG APPEND_PRESET_LOCAL_PLUGINS
|
ARG APPEND_PRESET_LOCAL_PLUGINS
|
||||||
@ -7,10 +7,17 @@ ARG PLUGINS_DIRS
|
|||||||
|
|
||||||
ENV PLUGINS_DIRS=${PLUGINS_DIRS}
|
ENV PLUGINS_DIRS=${PLUGINS_DIRS}
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y jq expect
|
||||||
|
|
||||||
RUN npx npm-cli-adduser --username test --password test -e test@nocobase.com -r $VERDACCIO_URL
|
RUN expect <<EOD
|
||||||
|
spawn npm adduser --registry $VERDACCIO_URL
|
||||||
|
expect {
|
||||||
|
"Username:" {send "test\r"; exp_continue}
|
||||||
|
"Password:" {send "test\r"; exp_continue}
|
||||||
|
"Email: (this IS public)" {send "test@nocobase.com\r"; exp_continue}
|
||||||
|
}
|
||||||
|
EOD
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y jq
|
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
COPY . /tmp
|
COPY . /tmp
|
||||||
RUN yarn install && yarn build --no-dts
|
RUN yarn install && yarn build --no-dts
|
||||||
@ -47,12 +54,12 @@ RUN cd /app \
|
|||||||
&& tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app .
|
&& tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app .
|
||||||
|
|
||||||
|
|
||||||
FROM node:20.13-bullseye-slim
|
FROM node:20-bookworm-slim
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends wget gnupg \
|
RUN apt-get update && apt-get install -y --no-install-recommends wget gnupg \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN sh -c 'echo "deb http://mirrors.ustc.edu.cn/postgresql/repos/apt bullseye-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
RUN sh -c 'echo "deb http://mirrors.ustc.edu.cn/postgresql/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||||
RUN wget --quiet -O - http://mirrors.ustc.edu.cn/postgresql/repos/apt/ACCC4CF8.asc | apt-key add -
|
RUN wget --quiet -O - http://mirrors.ustc.edu.cn/postgresql/repos/apt/ACCC4CF8.asc | apt-key add -
|
||||||
|
|
||||||
RUN apt-get update && apt-get -y --no-install-recommends install nginx libaio1 postgresql-client-16 postgresql-client-17 \
|
RUN apt-get update && apt-get -y --no-install-recommends install nginx libaio1 postgresql-client-16 postgresql-client-17 \
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM node:18-bullseye-slim as builder
|
FROM node:20-bookworm-slim as builder
|
||||||
|
|
||||||
ARG CNA_VERSION
|
ARG CNA_VERSION
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ RUN cd /app \
|
|||||||
&& rm -rf nocobase.tar.gz \
|
&& rm -rf nocobase.tar.gz \
|
||||||
&& tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app .
|
&& tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app .
|
||||||
|
|
||||||
FROM node:18-bullseye-slim
|
FROM node:20-bookworm-slim
|
||||||
|
|
||||||
# COPY ./sources.list /etc/apt/sources.list
|
# COPY ./sources.list /etc/apt/sources.list
|
||||||
RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \
|
RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
"version": "1.6.0-alpha.9",
|
"version": "1.6.0-alpha.9",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"npmClientArgs": [
|
"npmClientArgs": ["--ignore-engines"],
|
||||||
"--ignore-engines"
|
|
||||||
],
|
|
||||||
"command": {
|
"command": {
|
||||||
"version": {
|
"version": {
|
||||||
"forcePublish": true,
|
"forcePublish": true,
|
||||||
|
@ -31,6 +31,10 @@ interface IAuth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class Auth implements IAuth {
|
export abstract class Auth implements IAuth {
|
||||||
|
/**
|
||||||
|
* options keys that are not allowed to use environment variables
|
||||||
|
*/
|
||||||
|
public static optionsKeysNotAllowedInEnv: string[];
|
||||||
abstract user: Model;
|
abstract user: Model;
|
||||||
protected authenticator: Authenticator;
|
protected authenticator: Authenticator;
|
||||||
protected options: {
|
protected options: {
|
||||||
|
@ -99,6 +99,7 @@ export class Application {
|
|||||||
public schemaSettingsManager: SchemaSettingsManager;
|
public schemaSettingsManager: SchemaSettingsManager;
|
||||||
public dataSourceManager: DataSourceManager;
|
public dataSourceManager: DataSourceManager;
|
||||||
public name: string;
|
public name: string;
|
||||||
|
public globalVars: Record<string, any> = {};
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
maintained = false;
|
maintained = false;
|
||||||
@ -465,4 +466,12 @@ export class Application {
|
|||||||
componentOption,
|
componentOption,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addGlobalVar(key: string, value: any) {
|
||||||
|
set(this.globalVars, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
getGlobalVar(key) {
|
||||||
|
return get(this.globalVars, key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,3 +11,4 @@ export * from './useApp';
|
|||||||
export * from './useAppSpin';
|
export * from './useAppSpin';
|
||||||
export * from './usePlugin';
|
export * from './usePlugin';
|
||||||
export * from './useRouter';
|
export * from './useRouter';
|
||||||
|
export * from './useGlobalVariable';
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isFunction } from 'lodash';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useApp } from './';
|
||||||
|
|
||||||
|
export const useGlobalVariable = (key: string) => {
|
||||||
|
const app = useApp();
|
||||||
|
|
||||||
|
const variable = useMemo(() => {
|
||||||
|
return app.getGlobalVar(key);
|
||||||
|
}, [app, key]);
|
||||||
|
|
||||||
|
if (isFunction(variable)) {
|
||||||
|
try {
|
||||||
|
return variable();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error calling global variable function for key: ${key}`, error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return variable;
|
||||||
|
};
|
@ -72,7 +72,7 @@ export const enableLinkSettingsItem: SchemaSettingsItemType = {
|
|||||||
const { fieldSchema: columnSchema } = useColumnSchema();
|
const { fieldSchema: columnSchema } = useColumnSchema();
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
const fieldSchema = columnSchema || schema;
|
const fieldSchema = columnSchema || schema;
|
||||||
const { name } = useBlockContext();
|
const { name } = useBlockContext() || {};
|
||||||
return name !== 'kanban' && (fieldSchema?.['x-read-pretty'] || field.readPretty);
|
return name !== 'kanban' && (fieldSchema?.['x-read-pretty'] || field.readPretty);
|
||||||
},
|
},
|
||||||
useComponentProps() {
|
useComponentProps() {
|
||||||
|
@ -7,13 +7,13 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
import { Field } from '@formily/core';
|
import { Field } from '@formily/core';
|
||||||
import { useField } from '@formily/react';
|
import { useField } from '@formily/react';
|
||||||
import { Input } from 'antd';
|
import { Input } from 'antd';
|
||||||
import { TextAreaProps } from 'antd/es/input';
|
import { TextAreaProps } from 'antd/es/input';
|
||||||
import React, { useState, useEffect, Ref } from 'react';
|
|
||||||
import { cx, css } from '@emotion/css';
|
|
||||||
import JSON5 from 'json5';
|
import JSON5 from 'json5';
|
||||||
|
import React, { Ref, useEffect, useState } from 'react';
|
||||||
|
|
||||||
export type JSONTextAreaProps = TextAreaProps & { value?: string; space?: number; json5?: boolean };
|
export type JSONTextAreaProps = TextAreaProps & { value?: string; space?: number; json5?: boolean };
|
||||||
|
|
||||||
@ -30,7 +30,16 @@ export const Json = React.forwardRef<typeof Input.TextArea, JSONTextAreaProps>(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
setText(_JSON.stringify(value, null, space));
|
if (typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
_JSON.parse(value);
|
||||||
|
setText(value);
|
||||||
|
} catch (error) {
|
||||||
|
setText(_JSON.stringify(value, null, space));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setText(_JSON.stringify(value, null, space));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setText(undefined);
|
setText(undefined);
|
||||||
}
|
}
|
||||||
|
@ -223,7 +223,7 @@ function useVariablesFromValue(value: string, delimiters: [string, string] = ['{
|
|||||||
|
|
||||||
export function TextArea(props) {
|
export function TextArea(props) {
|
||||||
const { wrapSSR, hashId, componentCls } = useStyles();
|
const { wrapSSR, hashId, componentCls } = useStyles();
|
||||||
const { scope, onChange, changeOnSelect, style, fieldNames, delimiters = ['{{', '}}'] } = props;
|
const { scope, onChange, changeOnSelect, style, fieldNames, delimiters = ['{{', '}}'], addonBefore } = props;
|
||||||
const value = typeof props.value === 'string' ? props.value : props.value == null ? '' : props.value.toString();
|
const value = typeof props.value === 'string' ? props.value : props.value == null ? '' : props.value.toString();
|
||||||
const variables = useVariablesFromValue(value, delimiters);
|
const variables = useVariablesFromValue(value, delimiters);
|
||||||
const inputRef = useRef<HTMLDivElement>(null);
|
const inputRef = useRef<HTMLDivElement>(null);
|
||||||
@ -397,7 +397,6 @@ export function TextArea(props) {
|
|||||||
},
|
},
|
||||||
[onChange, delimitersString],
|
[onChange, delimitersString],
|
||||||
);
|
);
|
||||||
|
|
||||||
const disabled = props.disabled || form.disabled;
|
const disabled = props.disabled || form.disabled;
|
||||||
return wrapSSR(
|
return wrapSSR(
|
||||||
<Space.Compact
|
<Space.Compact
|
||||||
@ -410,6 +409,8 @@ export function TextArea(props) {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
border-top-left-radius: ${addonBefore ? '0px' : '6px'};
|
||||||
|
border-bottom-left-radius: ${addonBefore ? '0px' : '6px'};
|
||||||
}
|
}
|
||||||
.ant-input-disabled {
|
.ant-input-disabled {
|
||||||
.ant-tag {
|
.ant-tag {
|
||||||
@ -424,6 +425,19 @@ export function TextArea(props) {
|
|||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{addonBefore && (
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
border: 1px solid rgb(217, 217, 217);
|
||||||
|
padding: 0px 11px;
|
||||||
|
border-radius: 6px 0px 0px 6px;
|
||||||
|
border-right: 0px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{addonBefore}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
aria-label="textbox"
|
aria-label="textbox"
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { connect, mapReadPretty } from '@formily/react';
|
||||||
|
import { TextArea } from './TextArea';
|
||||||
|
import { RawTextArea } from './RawTextArea';
|
||||||
|
import { Password } from '../password';
|
||||||
|
import { Variable } from './Variable';
|
||||||
|
import { Input } from '../input';
|
||||||
|
import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable';
|
||||||
|
|
||||||
|
export const useEnvironmentVariableOptions = (scope) => {
|
||||||
|
const environmentVariables = useGlobalVariable('$env');
|
||||||
|
return useMemo(() => {
|
||||||
|
if (environmentVariables) {
|
||||||
|
return [environmentVariables].filter(Boolean);
|
||||||
|
}
|
||||||
|
return scope;
|
||||||
|
}, [environmentVariables, scope]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isVariable = (value) => {
|
||||||
|
const regex = /{{.*?}}/;
|
||||||
|
return regex.test(value);
|
||||||
|
};
|
||||||
|
interface TextAreaWithGlobalScopeProps {
|
||||||
|
supportsLineBreak?: boolean;
|
||||||
|
password?: boolean;
|
||||||
|
number?: boolean;
|
||||||
|
boolean?: boolean;
|
||||||
|
value?: any;
|
||||||
|
scope?: string | object;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextAreaWithGlobalScope = connect((props: TextAreaWithGlobalScopeProps) => {
|
||||||
|
const { supportsLineBreak, password, number, boolean, ...others } = props;
|
||||||
|
const scope = useEnvironmentVariableOptions(props.scope);
|
||||||
|
const fieldNames = { value: 'name', label: 'title' };
|
||||||
|
|
||||||
|
if (supportsLineBreak) {
|
||||||
|
return <RawTextArea {...others} scope={scope} fieldNames={fieldNames} rows={3} />;
|
||||||
|
}
|
||||||
|
if (number) {
|
||||||
|
return <Variable.Input {...props} scope={scope} fieldNames={fieldNames} />;
|
||||||
|
}
|
||||||
|
if (password && props.value && !isVariable(props.value)) {
|
||||||
|
return <Password {...others} autoFocus />;
|
||||||
|
}
|
||||||
|
if (boolean) {
|
||||||
|
return <Variable.Input {...props} scope={scope} fieldNames={fieldNames} />;
|
||||||
|
}
|
||||||
|
return <TextArea {...others} scope={scope} fieldNames={fieldNames} />;
|
||||||
|
}, mapReadPretty(Input.ReadPretty));
|
@ -8,3 +8,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './Variable';
|
export * from './Variable';
|
||||||
|
export { TextAreaWithGlobalScope } from './TextAreaWithGlobalScope';
|
||||||
|
@ -11,7 +11,7 @@ import { Result } from 'ahooks/es/useRequest/src/types';
|
|||||||
import React, { createContext, ReactNode, useContext } from 'react';
|
import React, { createContext, ReactNode, useContext } from 'react';
|
||||||
import { useRequest } from '../api-client';
|
import { useRequest } from '../api-client';
|
||||||
|
|
||||||
export const SystemSettingsContext = createContext<Result<any, any>>(null);
|
export const SystemSettingsContext = createContext<Result<any, any> | any>(null);
|
||||||
SystemSettingsContext.displayName = 'SystemSettingsContext';
|
SystemSettingsContext.displayName = 'SystemSettingsContext';
|
||||||
|
|
||||||
export const useSystemSettings = () => {
|
export const useSystemSettings = () => {
|
||||||
@ -20,8 +20,7 @@ export const useSystemSettings = () => {
|
|||||||
|
|
||||||
export const SystemSettingsProvider: React.FC<{ children?: ReactNode }> = (props) => {
|
export const SystemSettingsProvider: React.FC<{ children?: ReactNode }> = (props) => {
|
||||||
const result = useRequest({
|
const result = useRequest({
|
||||||
url: 'systemSettings:get/1?appends=logo',
|
url: 'systemSettings:get',
|
||||||
});
|
});
|
||||||
|
return <SystemSettingsContext.Provider value={{ ...result }}>{props.children}</SystemSettingsContext.Provider>;
|
||||||
return <SystemSettingsContext.Provider value={result}>{props.children}</SystemSettingsContext.Provider>;
|
|
||||||
};
|
};
|
||||||
|
@ -60,7 +60,7 @@ const useSaveSystemSettingsValues = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await api.request({
|
await api.request({
|
||||||
url: 'systemSettings:update/1',
|
url: 'systemSettings:put',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: values,
|
data: values,
|
||||||
});
|
});
|
||||||
@ -88,11 +88,14 @@ const schema: ISchema = {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{t("System settings")}}',
|
title: '{{t("System settings")}}',
|
||||||
properties: {
|
properties: {
|
||||||
title: {
|
raw_title: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: "{{t('System title')}}",
|
title: "{{t('System title')}}",
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input.TextArea',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
|
'x-component-props': {
|
||||||
|
supportsLineBreak: true,
|
||||||
|
},
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
logo: {
|
logo: {
|
||||||
|
@ -59,7 +59,6 @@ const VariablesProvider = ({ children, filterVariables }: any) => {
|
|||||||
const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated();
|
const { getCollectionJoinField, getCollection } = useCollectionManager_deprecated();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { builtinVariables } = useBuiltInVariables();
|
const { builtinVariables } = useBuiltInVariables();
|
||||||
|
|
||||||
const setCtx = useCallback((ctx: Record<string, any> | ((prev: Record<string, any>) => Record<string, any>)) => {
|
const setCtx = useCallback((ctx: Record<string, any> | ((prev: Record<string, any>) => Record<string, any>)) => {
|
||||||
if (_.isFunction(ctx)) {
|
if (_.isFunction(ctx)) {
|
||||||
ctxRef.current = ctx(ctxRef.current);
|
ctxRef.current = ctx(ctxRef.current);
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Collection } from './collection';
|
import { Collection } from './collection';
|
||||||
|
import { DataSource } from './data-source';
|
||||||
|
import { Repository } from './repository';
|
||||||
import {
|
import {
|
||||||
CollectionOptions,
|
CollectionOptions,
|
||||||
ICollection,
|
ICollection,
|
||||||
@ -16,8 +18,6 @@ import {
|
|||||||
IRepository,
|
IRepository,
|
||||||
MergeOptions,
|
MergeOptions,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { DataSource } from './data-source';
|
|
||||||
import { Repository } from './repository';
|
|
||||||
|
|
||||||
export class CollectionManager implements ICollectionManager {
|
export class CollectionManager implements ICollectionManager {
|
||||||
public dataSource: DataSource;
|
public dataSource: DataSource;
|
||||||
@ -35,6 +35,10 @@ export class CollectionManager implements ICollectionManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDataSource(dataSource: DataSource) {
|
||||||
|
this.dataSource = dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
/* istanbul ignore next -- @preserve */
|
/* istanbul ignore next -- @preserve */
|
||||||
getRegisteredFieldType(type) {}
|
getRegisteredFieldType(type) {}
|
||||||
|
|
||||||
|
@ -7,9 +7,9 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CollectionOptions, ICollection, ICollectionManager, IField, IRepository } from './types';
|
|
||||||
import { default as lodash } from 'lodash';
|
import { default as lodash } from 'lodash';
|
||||||
import { CollectionField } from './collection-field';
|
import { CollectionField } from './collection-field';
|
||||||
|
import { CollectionOptions, ICollection, ICollectionManager, IField, IRepository } from './types';
|
||||||
|
|
||||||
export class Collection implements ICollection {
|
export class Collection implements ICollection {
|
||||||
repository: IRepository;
|
repository: IRepository;
|
||||||
@ -17,7 +17,7 @@ export class Collection implements ICollection {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected options: CollectionOptions,
|
protected options: CollectionOptions,
|
||||||
protected collectionManager: ICollectionManager,
|
public collectionManager: ICollectionManager,
|
||||||
) {
|
) {
|
||||||
this.setRepository(options.repository);
|
this.setRepository(options.repository);
|
||||||
|
|
||||||
|
@ -8,10 +8,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { DataSource } from './data-source';
|
import { DataSource } from './data-source';
|
||||||
|
import { DataSourceManager } from './data-source-manager';
|
||||||
|
|
||||||
export class DataSourceFactory {
|
export class DataSourceFactory {
|
||||||
public collectionTypes: Map<string, typeof DataSource> = new Map();
|
public collectionTypes: Map<string, typeof DataSource> = new Map();
|
||||||
|
|
||||||
|
constructor(protected dataSourceManager: DataSourceManager) {}
|
||||||
|
|
||||||
register(type: string, dataSourceClass: typeof DataSource) {
|
register(type: string, dataSourceClass: typeof DataSource) {
|
||||||
this.collectionTypes.set(type, dataSourceClass);
|
this.collectionTypes.set(type, dataSourceClass);
|
||||||
}
|
}
|
||||||
@ -25,8 +28,17 @@ export class DataSourceFactory {
|
|||||||
if (!klass) {
|
if (!klass) {
|
||||||
throw new Error(`Data source type "${type}" not found`);
|
throw new Error(`Data source type "${type}" not found`);
|
||||||
}
|
}
|
||||||
|
const environment = this.dataSourceManager.options.app?.environment;
|
||||||
|
const { logger, sqlLogger, ...others } = options;
|
||||||
|
|
||||||
|
const opts = { logger, sqlLogger, ...others };
|
||||||
|
|
||||||
|
if (environment) {
|
||||||
|
Object.assign(opts, environment.renderJsonTemplate(others));
|
||||||
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return new klass(options);
|
const dataSource = new klass(opts) as DataSource;
|
||||||
|
dataSource.setDataSourceManager(this.dataSourceManager);
|
||||||
|
return dataSource;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,10 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { createConsoleLogger, createLogger, Logger, LoggerOptions } from '@nocobase/logger';
|
||||||
import { ToposortOptions } from '@nocobase/utils';
|
import { ToposortOptions } from '@nocobase/utils';
|
||||||
import { DataSource } from './data-source';
|
import { DataSource } from './data-source';
|
||||||
import { DataSourceFactory } from './data-source-factory';
|
import { DataSourceFactory } from './data-source-factory';
|
||||||
import { createConsoleLogger, createLogger, Logger, LoggerOptions } from '@nocobase/logger';
|
|
||||||
|
|
||||||
type DataSourceHook = (dataSource: DataSource) => void;
|
type DataSourceHook = (dataSource: DataSource) => void;
|
||||||
|
|
||||||
@ -24,13 +24,14 @@ export class DataSourceManager {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
factory: DataSourceFactory = new DataSourceFactory();
|
factory: DataSourceFactory;
|
||||||
protected middlewares = [];
|
protected middlewares = [];
|
||||||
private onceHooks: Array<DataSourceHook> = [];
|
private onceHooks: Array<DataSourceHook> = [];
|
||||||
private beforeAddHooks: Array<DataSourceHook> = [];
|
private beforeAddHooks: Array<DataSourceHook> = [];
|
||||||
|
|
||||||
constructor(public options: DataSourceManagerOptions = {}) {
|
constructor(public options: DataSourceManagerOptions = {}) {
|
||||||
this.dataSources = new Map();
|
this.dataSources = new Map();
|
||||||
|
this.factory = new DataSourceFactory(this);
|
||||||
this.middlewares = [];
|
this.middlewares = [];
|
||||||
|
|
||||||
if (options.app) {
|
if (options.app) {
|
||||||
|
@ -8,13 +8,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ACL } from '@nocobase/acl';
|
import { ACL } from '@nocobase/acl';
|
||||||
|
import { Logger } from '@nocobase/logger';
|
||||||
import { getNameByParams, parseRequest, ResourceManager } from '@nocobase/resourcer';
|
import { getNameByParams, parseRequest, ResourceManager } from '@nocobase/resourcer';
|
||||||
|
import { wrapMiddlewareWithLogging } from '@nocobase/utils';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import compose from 'koa-compose';
|
import compose from 'koa-compose';
|
||||||
|
import { DataSourceManager } from './data-source-manager';
|
||||||
import { loadDefaultActions } from './load-default-actions';
|
import { loadDefaultActions } from './load-default-actions';
|
||||||
import { ICollectionManager } from './types';
|
import { ICollectionManager } from './types';
|
||||||
import { Logger } from '@nocobase/logger';
|
|
||||||
import { wrapMiddlewareWithLogging } from '@nocobase/utils';
|
|
||||||
|
|
||||||
export type DataSourceOptions = any;
|
export type DataSourceOptions = any;
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ export abstract class DataSource extends EventEmitter {
|
|||||||
public collectionManager: ICollectionManager;
|
public collectionManager: ICollectionManager;
|
||||||
public resourceManager: ResourceManager;
|
public resourceManager: ResourceManager;
|
||||||
public acl: ACL;
|
public acl: ACL;
|
||||||
|
public dataSourceManager: DataSourceManager;
|
||||||
|
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
|
||||||
@ -49,6 +51,10 @@ export abstract class DataSource extends EventEmitter {
|
|||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDataSourceManager(dataSourceManager: DataSourceManager) {
|
||||||
|
this.dataSourceManager = dataSourceManager;
|
||||||
|
}
|
||||||
|
|
||||||
setLogger(logger: Logger) {
|
setLogger(logger: Logger) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
@ -66,6 +72,9 @@ export abstract class DataSource extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.collectionManager = this.createCollectionManager(options);
|
this.collectionManager = this.createCollectionManager(options);
|
||||||
|
if (this.collectionManager) {
|
||||||
|
this.collectionManager.setDataSource(this);
|
||||||
|
}
|
||||||
this.resourceManager.registerActionHandlers(loadDefaultActions());
|
this.resourceManager.registerActionHandlers(loadDefaultActions());
|
||||||
|
|
||||||
if (options.acl !== false) {
|
if (options.acl !== false) {
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
/* istanbul ignore file -- @preserve */
|
/* istanbul ignore file -- @preserve */
|
||||||
|
|
||||||
import { Database } from '@nocobase/database';
|
import { Database } from '@nocobase/database';
|
||||||
|
import { DataSource } from './data-source';
|
||||||
import {
|
import {
|
||||||
CollectionOptions,
|
CollectionOptions,
|
||||||
ICollection,
|
ICollection,
|
||||||
@ -22,12 +23,17 @@ import {
|
|||||||
export class SequelizeCollectionManager implements ICollectionManager {
|
export class SequelizeCollectionManager implements ICollectionManager {
|
||||||
db: Database;
|
db: Database;
|
||||||
options: any;
|
options: any;
|
||||||
|
dataSource: DataSource;
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.db = this.createDB(options);
|
this.db = this.createDB(options);
|
||||||
this.options = options;
|
this.options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDataSource(dataSource: DataSource) {
|
||||||
|
this.dataSource = dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
collectionsFilter() {
|
collectionsFilter() {
|
||||||
if (this.options.collectionsFilter) {
|
if (this.options.collectionsFilter) {
|
||||||
return this.options.collectionsFilter;
|
return this.options.collectionsFilter;
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { DataSource } from './data-source';
|
||||||
|
|
||||||
export type CollectionOptions = {
|
export type CollectionOptions = {
|
||||||
name: string;
|
name: string;
|
||||||
repository?: string;
|
repository?: string;
|
||||||
@ -102,6 +104,10 @@ export type MergeOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface ICollectionManager {
|
export interface ICollectionManager {
|
||||||
|
dataSource: DataSource;
|
||||||
|
|
||||||
|
setDataSource(dataSource: DataSource): void;
|
||||||
|
|
||||||
registerFieldTypes(types: Record<string, any>): void;
|
registerFieldTypes(types: Record<string, any>): void;
|
||||||
|
|
||||||
registerFieldInterfaces(interfaces: Record<string, new (options: any) => IFieldInterface>): void;
|
registerFieldInterfaces(interfaces: Record<string, new (options: any) => IFieldInterface>): void;
|
||||||
|
@ -147,7 +147,7 @@ describe('underscored options', () => {
|
|||||||
type: 'belongsToMany',
|
type: 'belongsToMany',
|
||||||
name: 'tags',
|
name: 'tags',
|
||||||
through: 'collectionCategory',
|
through: 'collectionCategory',
|
||||||
target: 'posts',
|
target: 'tags',
|
||||||
sourceKey: 'name',
|
sourceKey: 'name',
|
||||||
foreignKey: 'postsName',
|
foreignKey: 'postsName',
|
||||||
targetKey: 'name',
|
targetKey: 'name',
|
||||||
|
@ -88,16 +88,19 @@ export type DumpRules =
|
|||||||
| ({ skipped: true } & BaseDumpRules)
|
| ({ skipped: true } & BaseDumpRules)
|
||||||
| ({ group: BuiltInGroup | string } & BaseDumpRules);
|
| ({ group: BuiltInGroup | string } & BaseDumpRules);
|
||||||
|
|
||||||
|
export type MigrationRule = 'overwrite' | 'skip' | 'upsert' | 'schema-only' | 'insert-ignore';
|
||||||
|
|
||||||
export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'> {
|
export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'> {
|
||||||
name: string;
|
name: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
|
migrationRules?: MigrationRule[];
|
||||||
dumpRules?: DumpRules;
|
dumpRules?: DumpRules;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
inherits?: string[] | string;
|
inherits?: string[] | string;
|
||||||
viewName?: string;
|
viewName?: string;
|
||||||
writableView?: boolean;
|
writableView?: boolean;
|
||||||
|
isThrough?: boolean;
|
||||||
filterTargetKey?: string | string[];
|
filterTargetKey?: string | string[];
|
||||||
fields?: FieldOptions[];
|
fields?: FieldOptions[];
|
||||||
model?: string | ModelStatic<Model>;
|
model?: string | ModelStatic<Model>;
|
||||||
|
@ -309,6 +309,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['schema-only', 'overwrite', 'skip'],
|
||||||
origin: '@nocobase/database',
|
origin: '@nocobase/database',
|
||||||
fields: [{ type: 'string', name: 'name', primaryKey: true }],
|
fields: [{ type: 'string', name: 'name', primaryKey: true }],
|
||||||
});
|
});
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { lodash } from '@nocobase/utils';
|
||||||
import { BaseDialect } from './base-dialect';
|
import { BaseDialect } from './base-dialect';
|
||||||
|
|
||||||
export class MysqlDialect extends BaseDialect {
|
export class MysqlDialect extends BaseDialect {
|
||||||
@ -22,4 +23,9 @@ export class MysqlDialect extends BaseDialect {
|
|||||||
version: '>=8.0.17',
|
version: '>=8.0.17',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSequelizeOptions(options: any) {
|
||||||
|
lodash.set(options, 'dialectOptions.multipleStatements', true);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,6 +144,9 @@ export class BelongsToManyField extends RelationField {
|
|||||||
} else {
|
} else {
|
||||||
const throughCollectionOptions = {
|
const throughCollectionOptions = {
|
||||||
name: through,
|
name: through,
|
||||||
|
isThrough: true,
|
||||||
|
sourceCollectionName: this.collection.name,
|
||||||
|
targetCollectionName: this.target,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.collection.options.dumpRules) {
|
if (this.collection.options.dumpRules) {
|
||||||
|
@ -76,6 +76,7 @@ import packageJson from '../package.json';
|
|||||||
import { ServiceContainer } from './service-container';
|
import { ServiceContainer } from './service-container';
|
||||||
import { availableActions } from './acl/available-action';
|
import { availableActions } from './acl/available-action';
|
||||||
import { AuditManager } from './audit-manager';
|
import { AuditManager } from './audit-manager';
|
||||||
|
import { Environment } from './environment';
|
||||||
|
|
||||||
export type PluginType = string | typeof Plugin;
|
export type PluginType = string | typeof Plugin;
|
||||||
export type PluginConfiguration = PluginType | [PluginType, any];
|
export type PluginConfiguration = PluginType | [PluginType, any];
|
||||||
@ -309,6 +310,12 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
return this._maintainingMessage;
|
return this._maintainingMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _env: Environment;
|
||||||
|
|
||||||
|
get environment() {
|
||||||
|
return this._env;
|
||||||
|
}
|
||||||
|
|
||||||
protected _cronJobManager: CronJobManager;
|
protected _cronJobManager: CronJobManager;
|
||||||
|
|
||||||
get cronJobManager() {
|
get cronJobManager() {
|
||||||
@ -1183,6 +1190,7 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
this.createMainDataSource(options);
|
this.createMainDataSource(options);
|
||||||
|
|
||||||
this._cronJobManager = new CronJobManager(this);
|
this._cronJobManager = new CronJobManager(this);
|
||||||
|
this._env = new Environment();
|
||||||
|
|
||||||
this._cli = this.createCLI();
|
this._cli = this.createCLI();
|
||||||
this._i18n = createI18n(options);
|
this._i18n = createI18n(options);
|
||||||
|
47
packages/core/server/src/environment.ts
Normal file
47
packages/core/server/src/environment.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { parse } from '@nocobase/utils';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export class Environment {
|
||||||
|
private vars = {};
|
||||||
|
|
||||||
|
setVariable(key: string, value: string) {
|
||||||
|
this.vars[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeVariable(key: string) {
|
||||||
|
delete this.vars[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariablesAndSecrets() {
|
||||||
|
return this.vars;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariables() {
|
||||||
|
return this.vars;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderJsonTemplate(template: any, options?: { omit?: string[] }) {
|
||||||
|
if (options?.omit) {
|
||||||
|
const omitTemplate = _.omit(template, options.omit);
|
||||||
|
const parsed = parse(omitTemplate)({
|
||||||
|
$env: this.vars,
|
||||||
|
});
|
||||||
|
for (const key of options.omit) {
|
||||||
|
_.set(parsed, key, _.get(template, key));
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return parse(template)({
|
||||||
|
$env: this.vars,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ export class ApplicationVersion {
|
|||||||
app.db.collection({
|
app.db.collection({
|
||||||
origin: '@nocobase/server',
|
origin: '@nocobase/server',
|
||||||
name: 'applicationVersion',
|
name: 'applicationVersion',
|
||||||
|
migrationRules: ['schema-only', 'skip'],
|
||||||
dataType: 'meta',
|
dataType: 'meta',
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
@ -12,6 +12,7 @@ import { defineCollection } from '@nocobase/database';
|
|||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
name: 'applicationPlugins',
|
name: 'applicationPlugins',
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
repository: 'PluginManagerRepository',
|
repository: 'PluginManagerRepository',
|
||||||
origin: '@nocobase/server',
|
origin: '@nocobase/server',
|
||||||
fields: [
|
fields: [
|
||||||
|
@ -18,6 +18,7 @@ export * from './mock-server';
|
|||||||
|
|
||||||
export const pgOnly: () => any = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip);
|
export const pgOnly: () => any = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip);
|
||||||
export const isPg = () => process.env.DB_DIALECT == 'postgres';
|
export const isPg = () => process.env.DB_DIALECT == 'postgres';
|
||||||
|
export const isMysql = () => process.env.DB_DIALECT == 'mysql';
|
||||||
|
|
||||||
export function randomStr() {
|
export function randomStr() {
|
||||||
// create random string
|
// create random string
|
||||||
|
@ -11,8 +11,10 @@ import { defineCollection } from '@nocobase/database';
|
|||||||
|
|
||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
name: 'rolesUsers',
|
name: 'rolesUsers',
|
||||||
|
description: "User's roles",
|
||||||
dumpRules: {
|
dumpRules: {
|
||||||
group: 'user',
|
group: 'user',
|
||||||
},
|
},
|
||||||
|
migrationRules: ['schema-only', 'overwrite', 'skip'],
|
||||||
fields: [{ type: 'boolean', name: 'default' }],
|
fields: [{ type: 'boolean', name: 'default' }],
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,8 @@ import { defineCollection } from '@nocobase/database';
|
|||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
origin: '@nocobase/plugin-acl',
|
origin: '@nocobase/plugin-acl',
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
description: 'Role data',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
name: 'roles',
|
name: 'roles',
|
||||||
title: '{{t("Roles")}}',
|
title: '{{t("Roles")}}',
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
|
@ -12,6 +12,7 @@ import { defineCollection } from '@nocobase/database';
|
|||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
name: 'rolesResources',
|
name: 'rolesResources',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
model: 'RoleResourceModel',
|
model: 'RoleResourceModel',
|
||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
|
@ -12,6 +12,7 @@ import { defineCollection } from '@nocobase/database';
|
|||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
name: 'rolesResourcesActions',
|
name: 'rolesResourcesActions',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
model: 'RoleResourceActionModel',
|
model: 'RoleResourceActionModel',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
@ -12,6 +12,7 @@ import { defineCollection } from '@nocobase/database';
|
|||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
name: 'rolesResourcesScopes',
|
name: 'rolesResourcesScopes',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'uid',
|
type: 'uid',
|
||||||
|
@ -11,6 +11,7 @@ import { extendCollection } from '@nocobase/database';
|
|||||||
|
|
||||||
export default extendCollection({
|
export default extendCollection({
|
||||||
name: 'users',
|
name: 'users',
|
||||||
|
migrationRules: ['schema-only', 'overwrite', 'skip'],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
interface: 'm2m',
|
interface: 'm2m',
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
useRequest,
|
useRequest,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { App } from 'antd';
|
import { App } from 'antd';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { listByCurrentRoleUrl } from '../constants';
|
import { listByCurrentRoleUrl } from '../constants';
|
||||||
import { useCustomRequestVariableOptions, useGetCustomRequest } from '../hooks';
|
import { useCustomRequestVariableOptions, useGetCustomRequest } from '../hooks';
|
||||||
import { useCustomRequestsResource } from '../hooks/useCustomRequestsResource';
|
import { useCustomRequestsResource } from '../hooks/useCustomRequestsResource';
|
||||||
@ -33,9 +33,15 @@ export function CustomRequestSettingsItem() {
|
|||||||
const dataSourceKey = useDataSourceKey();
|
const dataSourceKey = useDataSourceKey();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const customRequestsResource = useCustomRequestsResource();
|
const customRequestsResource = useCustomRequestsResource();
|
||||||
const { message } = App.useApp();
|
|
||||||
const { data, refresh } = useGetCustomRequest();
|
const { data, refresh } = useGetCustomRequest();
|
||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
|
const initialValues = useMemo(() => {
|
||||||
|
const values = { ...data?.data?.options };
|
||||||
|
if (values.data && typeof values.data !== 'string') {
|
||||||
|
values.data = JSON.stringify(values.data, null, 2);
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}, [data?.data?.options]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SchemaSettingsActionModalItem
|
<SchemaSettingsActionModalItem
|
||||||
@ -46,9 +52,7 @@ export function CustomRequestSettingsItem() {
|
|||||||
beforeOpen={() => !data && refresh()}
|
beforeOpen={() => !data && refresh()}
|
||||||
scope={{ useCustomRequestVariableOptions }}
|
scope={{ useCustomRequestVariableOptions }}
|
||||||
schema={CustomRequestConfigurationFieldsSchema}
|
schema={CustomRequestConfigurationFieldsSchema}
|
||||||
initialValues={{
|
initialValues={initialValues}
|
||||||
...data?.data?.options,
|
|
||||||
}}
|
|
||||||
onSubmit={async (config) => {
|
onSubmit={async (config) => {
|
||||||
const { ...requestSettings } = config;
|
const { ...requestSettings } = config;
|
||||||
fieldSchema['x-response-type'] = requestSettings.responseType;
|
fieldSchema['x-response-type'] = requestSettings.responseType;
|
||||||
@ -69,7 +73,8 @@ export function CustomRequestSettingsItem() {
|
|||||||
'x-uid': fieldSchema['x-uid'],
|
'x-uid': fieldSchema['x-uid'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
message.success(t('Saved successfully'));
|
refresh();
|
||||||
|
dn.refresh();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
useCollectionFilterOptions,
|
useCollectionFilterOptions,
|
||||||
useCollectionRecordData,
|
useCollectionRecordData,
|
||||||
useCompile,
|
useCompile,
|
||||||
|
useGlobalVariable,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from '../locale';
|
import { useTranslation } from '../locale';
|
||||||
@ -26,13 +27,13 @@ export const useCustomRequestVariableOptions = () => {
|
|||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const recordData = useCollectionRecordData();
|
const recordData = useCollectionRecordData();
|
||||||
const { name: blockType } = useBlockContext() || {};
|
const { name: blockType } = useBlockContext() || {};
|
||||||
|
|
||||||
const [fields, userFields] = useMemo(() => {
|
const [fields, userFields] = useMemo(() => {
|
||||||
return [compile(fieldsOptions), compile(userFieldOptions)];
|
return [compile(fieldsOptions), compile(userFieldOptions)];
|
||||||
}, [fieldsOptions, userFieldOptions]);
|
}, [fieldsOptions, userFieldOptions]);
|
||||||
|
const environmentVariables = useGlobalVariable('$env');
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
environmentVariables,
|
||||||
recordData && {
|
recordData && {
|
||||||
name: 'currentRecord',
|
name: 'currentRecord',
|
||||||
title: t('Current record', { ns: 'client' }),
|
title: t('Current record', { ns: 'client' }),
|
||||||
|
@ -154,6 +154,7 @@ export async function send(this: CustomRequestPlugin, ctx: Context, next: Next)
|
|||||||
currentTime: new Date().toISOString(),
|
currentTime: new Date().toISOString(),
|
||||||
$nToken: ctx.getBearerToken(),
|
$nToken: ctx.getBearerToken(),
|
||||||
$nForm,
|
$nForm,
|
||||||
|
$env: ctx.app.environment.getVariables(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const axiosRequestConfig = {
|
const axiosRequestConfig = {
|
||||||
@ -169,8 +170,6 @@ export async function send(this: CustomRequestPlugin, ctx: Context, next: Next)
|
|||||||
data: getParsedValue(data, variables),
|
data: getParsedValue(data, variables),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(axiosRequestConfig);
|
|
||||||
|
|
||||||
const requestUrl = axios.getUri(axiosRequestConfig);
|
const requestUrl = axios.getUri(axiosRequestConfig);
|
||||||
this.logger.info(`custom-request:send:${filterByTk} request url ${requestUrl}`);
|
this.logger.info(`custom-request:send:${filterByTk} request url ${requestUrl}`);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
|
@ -13,6 +13,7 @@ export default defineCollection({
|
|||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
name: 'customRequests',
|
name: 'customRequests',
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'uid',
|
type: 'uid',
|
||||||
|
@ -12,4 +12,5 @@ import { defineCollection } from '@nocobase/database';
|
|||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
name: 'customRequestsRoles',
|
name: 'customRequestsRoles',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
import { Logger, LoggerOptions } from '@nocobase/logger';
|
import { Logger, LoggerOptions } from '@nocobase/logger';
|
||||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||||
import { resolve } from 'path';
|
|
||||||
import { listByCurrentRole } from './actions/listByCurrentRole';
|
import { listByCurrentRole } from './actions/listByCurrentRole';
|
||||||
import { send } from './actions/send';
|
import { send } from './actions/send';
|
||||||
|
|
||||||
@ -32,9 +31,7 @@ export class PluginActionCustomRequestServer extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
await this.importCollections(resolve(__dirname, 'collections'));
|
this.app.resourceManager.define({
|
||||||
|
|
||||||
this.app.resource({
|
|
||||||
name: 'customRequests',
|
name: 'customRequests',
|
||||||
actions: {
|
actions: {
|
||||||
send: send.bind(this),
|
send: send.bind(this),
|
||||||
|
@ -14,6 +14,7 @@ export default {
|
|||||||
dumpRules: {
|
dumpRules: {
|
||||||
group: 'user',
|
group: 'user',
|
||||||
},
|
},
|
||||||
|
migrationRules: ['schema-only', 'skip'],
|
||||||
shared: true,
|
shared: true,
|
||||||
name: 'apiKeys',
|
name: 'apiKeys',
|
||||||
sortable: 'sort',
|
sortable: 'sort',
|
||||||
|
@ -13,6 +13,7 @@ export default defineCollection({
|
|||||||
dumpRules: {
|
dumpRules: {
|
||||||
group: 'log',
|
group: 'log',
|
||||||
},
|
},
|
||||||
|
migrationRules: ['schema-only', 'skip'],
|
||||||
name: 'auditLogs',
|
name: 'auditLogs',
|
||||||
createdBy: false,
|
createdBy: false,
|
||||||
updatedBy: false,
|
updatedBy: false,
|
||||||
|
@ -16,6 +16,7 @@ export default defineCollection({
|
|||||||
dumpRules: {
|
dumpRules: {
|
||||||
group: 'third-party',
|
group: 'third-party',
|
||||||
},
|
},
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
shared: true,
|
shared: true,
|
||||||
name: 'authenticators',
|
name: 'authenticators',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
|
@ -13,6 +13,7 @@ export default defineCollection({
|
|||||||
dumpRules: {
|
dumpRules: {
|
||||||
group: 'log',
|
group: 'log',
|
||||||
},
|
},
|
||||||
|
migrationRules: ['schema-only', 'skip'],
|
||||||
shared: true,
|
shared: true,
|
||||||
name: 'tokenBlacklist',
|
name: 'tokenBlacklist',
|
||||||
model: 'TokenBlacklistModel',
|
model: 'TokenBlacklistModel',
|
||||||
|
@ -18,6 +18,7 @@ export default defineCollection({
|
|||||||
group: 'user',
|
group: 'user',
|
||||||
},
|
},
|
||||||
shared: true,
|
shared: true,
|
||||||
|
migrationRules: ['schema-only', 'overwrite', 'skip'],
|
||||||
name: 'usersAuthenticators',
|
name: 'usersAuthenticators',
|
||||||
model: 'UserAuthModel',
|
model: 'UserAuthModel',
|
||||||
createdBy: true,
|
createdBy: true,
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import { Cache } from '@nocobase/cache';
|
import { Cache } from '@nocobase/cache';
|
||||||
import { Model } from '@nocobase/database';
|
import { Model } from '@nocobase/database';
|
||||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||||
|
import { tval } from '@nocobase/utils';
|
||||||
import { namespace, presetAuthType, presetAuthenticator } from '../preset';
|
import { namespace, presetAuthType, presetAuthenticator } from '../preset';
|
||||||
import authActions from './actions/auth';
|
import authActions from './actions/auth';
|
||||||
import authenticatorsActions from './actions/authenticators';
|
import authenticatorsActions from './actions/authenticators';
|
||||||
@ -17,7 +18,6 @@ import { BasicAuth } from './basic-auth';
|
|||||||
import { AuthModel } from './model/authenticator';
|
import { AuthModel } from './model/authenticator';
|
||||||
import { Storer } from './storer';
|
import { Storer } from './storer';
|
||||||
import { TokenBlacklistService } from './token-blacklist';
|
import { TokenBlacklistService } from './token-blacklist';
|
||||||
import { tval } from '@nocobase/utils';
|
|
||||||
|
|
||||||
export class PluginAuthServer extends Plugin {
|
export class PluginAuthServer extends Plugin {
|
||||||
cache: Cache;
|
cache: Cache;
|
||||||
@ -36,8 +36,10 @@ export class PluginAuthServer extends Plugin {
|
|||||||
});
|
});
|
||||||
// Set up auth manager
|
// Set up auth manager
|
||||||
const storer = new Storer({
|
const storer = new Storer({
|
||||||
|
app: this.app,
|
||||||
db: this.db,
|
db: this.db,
|
||||||
cache: this.cache,
|
cache: this.cache,
|
||||||
|
authManager: this.app.authManager,
|
||||||
});
|
});
|
||||||
this.app.authManager.setStorer(storer);
|
this.app.authManager.setStorer(storer);
|
||||||
|
|
||||||
|
@ -7,32 +7,62 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Storer as IStorer } from '@nocobase/auth';
|
import { AuthManager, Storer as IStorer } from '@nocobase/auth';
|
||||||
import { Cache } from '@nocobase/cache';
|
import { Cache } from '@nocobase/cache';
|
||||||
import { Database, Model } from '@nocobase/database';
|
import { Database, Model } from '@nocobase/database';
|
||||||
|
import { Application } from '@nocobase/server';
|
||||||
import { AuthModel } from './model/authenticator';
|
import { AuthModel } from './model/authenticator';
|
||||||
|
|
||||||
export class Storer implements IStorer {
|
export class Storer implements IStorer {
|
||||||
db: Database;
|
db: Database;
|
||||||
cache: Cache;
|
cache: Cache;
|
||||||
|
app: Application;
|
||||||
|
authManager: AuthManager;
|
||||||
key = 'authenticators';
|
key = 'authenticators';
|
||||||
|
|
||||||
constructor({ db, cache }: { db: Database; cache: Cache }) {
|
constructor({
|
||||||
|
app,
|
||||||
|
db,
|
||||||
|
cache,
|
||||||
|
authManager,
|
||||||
|
}: {
|
||||||
|
app?: Application;
|
||||||
|
db: Database;
|
||||||
|
cache: Cache;
|
||||||
|
authManager: AuthManager;
|
||||||
|
}) {
|
||||||
|
this.app = app;
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
|
this.authManager = authManager;
|
||||||
|
|
||||||
this.db.on('authenticators.afterSave', async (model: AuthModel) => {
|
this.db.on('authenticators.afterSave', async (model: AuthModel) => {
|
||||||
if (!model.enabled) {
|
if (!model.enabled) {
|
||||||
await this.cache.delValueInObject(this.key, model.name);
|
await this.cache.delValueInObject(this.key, model.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.cache.setValueInObject(this.key, model.name, model);
|
await this.cache.setValueInObject(this.key, model.name, this.renderJsonTemplate(model));
|
||||||
});
|
});
|
||||||
this.db.on('authenticators.afterDestroy', async (model: AuthModel) => {
|
this.db.on('authenticators.afterDestroy', async (model: AuthModel) => {
|
||||||
await this.cache.delValueInObject(this.key, model.name);
|
await this.cache.delValueInObject(this.key, model.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderJsonTemplate(authenticator: any) {
|
||||||
|
if (!authenticator) {
|
||||||
|
return authenticator;
|
||||||
|
}
|
||||||
|
const $env = this.app?.environment;
|
||||||
|
if (!$env) {
|
||||||
|
return authenticator;
|
||||||
|
}
|
||||||
|
const config = this.authManager.getAuthConfig(authenticator.authType);
|
||||||
|
authenticator.dataValues.options = $env.renderJsonTemplate(authenticator.dataValues.options, {
|
||||||
|
omit: config?.auth?.['optionsKeysNotAllowedInEnv'],
|
||||||
|
});
|
||||||
|
return authenticator;
|
||||||
|
}
|
||||||
|
|
||||||
async getCache(): Promise<AuthModel[]> {
|
async getCache(): Promise<AuthModel[]> {
|
||||||
const authenticators = (await this.cache.get(this.key)) as Record<string, AuthModel>;
|
const authenticators = (await this.cache.get(this.key)) as Record<string, AuthModel>;
|
||||||
if (!authenticators) {
|
if (!authenticators) {
|
||||||
@ -43,7 +73,7 @@ export class Storer implements IStorer {
|
|||||||
|
|
||||||
async setCache(authenticators: AuthModel[]) {
|
async setCache(authenticators: AuthModel[]) {
|
||||||
const obj = authenticators.reduce((obj, authenticator) => {
|
const obj = authenticators.reduce((obj, authenticator) => {
|
||||||
obj[authenticator.name] = authenticator;
|
obj[authenticator.name] = this.renderJsonTemplate(authenticator);
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
await this.cache.set(this.key, obj);
|
await this.cache.set(this.key, obj);
|
||||||
@ -55,6 +85,7 @@ export class Storer implements IStorer {
|
|||||||
const repo = this.db.getRepository('authenticators');
|
const repo = this.db.getRepository('authenticators');
|
||||||
authenticators = await repo.find({ filter: { enabled: true } });
|
authenticators = await repo.find({ filter: { enabled: true } });
|
||||||
await this.setCache(authenticators);
|
await this.setCache(authenticators);
|
||||||
|
authenticators = await this.getCache();
|
||||||
}
|
}
|
||||||
const authenticator = authenticators.find((authenticator: Model) => authenticator.name === name);
|
const authenticator = authenticators.find((authenticator: Model) => authenticator.name === name);
|
||||||
return authenticator || authenticators[0];
|
return authenticator || authenticators[0];
|
||||||
|
@ -13,6 +13,7 @@ export default {
|
|||||||
namespace: 'iframe-block.iframe-html-storage',
|
namespace: 'iframe-block.iframe-html-storage',
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
name: 'iframeHtml',
|
name: 'iframeHtml',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
createdBy: true,
|
createdBy: true,
|
||||||
updatedBy: true,
|
updatedBy: true,
|
||||||
shared: true,
|
shared: true,
|
||||||
|
@ -8,11 +8,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Plugin } from '@nocobase/server';
|
import { Plugin } from '@nocobase/server';
|
||||||
|
import * as process from 'node:process';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { getAntdLocale } from './antd';
|
import { getAntdLocale } from './antd';
|
||||||
import { getCronLocale } from './cron';
|
import { getCronLocale } from './cron';
|
||||||
import { getCronstrueLocale } from './cronstrue';
|
import { getCronstrueLocale } from './cronstrue';
|
||||||
import * as process from 'node:process';
|
|
||||||
|
|
||||||
async function getLang(ctx) {
|
async function getLang(ctx) {
|
||||||
const SystemSetting = ctx.db.getRepository('systemSettings');
|
const SystemSetting = ctx.db.getRepository('systemSettings');
|
||||||
@ -66,11 +66,11 @@ export class PluginClientServer extends Plugin {
|
|||||||
this.app.acl.allow('app', 'getInfo');
|
this.app.acl.allow('app', 'getInfo');
|
||||||
this.app.acl.registerSnippet({
|
this.app.acl.registerSnippet({
|
||||||
name: 'app',
|
name: 'app',
|
||||||
actions: ['app:restart', 'app:clearCache'],
|
actions: ['app:restart', 'app:refresh', 'app:clearCache'],
|
||||||
});
|
});
|
||||||
const dialect = this.app.db.sequelize.getDialect();
|
const dialect = this.app.db.sequelize.getDialect();
|
||||||
|
|
||||||
this.app.resource({
|
this.app.resourceManager.define({
|
||||||
name: 'app',
|
name: 'app',
|
||||||
actions: {
|
actions: {
|
||||||
async getInfo(ctx, next) {
|
async getInfo(ctx, next) {
|
||||||
@ -116,10 +116,14 @@ export class PluginClientServer extends Plugin {
|
|||||||
ctx.app.runAsCLI(['restart'], { from: 'user' });
|
ctx.app.runAsCLI(['restart'], { from: 'user' });
|
||||||
await next();
|
await next();
|
||||||
},
|
},
|
||||||
|
async refresh(ctx, next) {
|
||||||
|
ctx.app.runCommand('refresh');
|
||||||
|
await next();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.app.auditManager.registerActions(['app:restart', 'app:clearCache']);
|
this.app.auditManager.registerActions(['app:restart', 'app:refresh', 'app:clearCache']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,9 +36,13 @@ class PluginCollectionTreeServer extends Plugin {
|
|||||||
|
|
||||||
//always define tree path collection
|
//always define tree path collection
|
||||||
const options = {};
|
const options = {};
|
||||||
|
|
||||||
|
options['mainCollection'] = collection.name;
|
||||||
|
|
||||||
if (collection.options.schema) {
|
if (collection.options.schema) {
|
||||||
options['schema'] = collection.options.schema;
|
options['schema'] = collection.options.schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.defineTreePathCollection(name, options);
|
this.defineTreePathCollection(name, options);
|
||||||
|
|
||||||
//afterSync
|
//afterSync
|
||||||
|
@ -13,6 +13,7 @@ export default {
|
|||||||
dumpRules: {
|
dumpRules: {
|
||||||
group: 'required',
|
group: 'required',
|
||||||
},
|
},
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
shared: true,
|
shared: true,
|
||||||
name: 'collectionCategories',
|
name: 'collectionCategories',
|
||||||
autoGenId: true,
|
autoGenId: true,
|
||||||
|
@ -11,6 +11,7 @@ import { CollectionOptions } from '@nocobase/database';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
shared: true,
|
shared: true,
|
||||||
name: 'collections',
|
name: 'collections',
|
||||||
sortable: 'sort',
|
sortable: 'sort',
|
||||||
|
@ -11,6 +11,7 @@ import { CollectionOptions } from '@nocobase/database';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
shared: true,
|
shared: true,
|
||||||
name: 'fields',
|
name: 'fields',
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
|
@ -13,6 +13,8 @@ import { Plugin } from '@nocobase/server';
|
|||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { CollectionRepository } from '.';
|
import { CollectionRepository } from '.';
|
||||||
|
import { FieldIsDependedOnByOtherError } from './errors/field-is-depended-on-by-other';
|
||||||
|
import { FieldNameExistsError } from './errors/field-name-exists-error';
|
||||||
import {
|
import {
|
||||||
afterCreateForForeignKeyField,
|
afterCreateForForeignKeyField,
|
||||||
afterCreateForReverseField,
|
afterCreateForReverseField,
|
||||||
@ -20,15 +22,13 @@ import {
|
|||||||
beforeDestroyForeignKey,
|
beforeDestroyForeignKey,
|
||||||
beforeInitOptions,
|
beforeInitOptions,
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
|
import { beforeCreateCheckFieldInMySQL } from './hooks/beforeCreateCheckFieldInMySQL';
|
||||||
import { beforeCreateForValidateField, beforeUpdateForValidateField } from './hooks/beforeCreateForValidateField';
|
import { beforeCreateForValidateField, beforeUpdateForValidateField } from './hooks/beforeCreateForValidateField';
|
||||||
import { beforeCreateForViewCollection } from './hooks/beforeCreateForViewCollection';
|
import { beforeCreateForViewCollection } from './hooks/beforeCreateForViewCollection';
|
||||||
|
import { beforeDestoryField } from './hooks/beforeDestoryField';
|
||||||
import { CollectionModel, FieldModel } from './models';
|
import { CollectionModel, FieldModel } from './models';
|
||||||
import collectionActions from './resourcers/collections';
|
import collectionActions from './resourcers/collections';
|
||||||
import viewResourcer from './resourcers/views';
|
import viewResourcer from './resourcers/views';
|
||||||
import { FieldNameExistsError } from './errors/field-name-exists-error';
|
|
||||||
import { beforeDestoryField } from './hooks/beforeDestoryField';
|
|
||||||
import { FieldIsDependedOnByOtherError } from './errors/field-is-depended-on-by-other';
|
|
||||||
import { beforeCreateCheckFieldInMySQL } from './hooks/beforeCreateCheckFieldInMySQL';
|
|
||||||
|
|
||||||
export class PluginDataSourceMainServer extends Plugin {
|
export class PluginDataSourceMainServer extends Plugin {
|
||||||
private loadFilter: Filter = {};
|
private loadFilter: Filter = {};
|
||||||
@ -394,7 +394,6 @@ export class PluginDataSourceMainServer extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
await this.importCollections(path.resolve(__dirname, './collections'));
|
|
||||||
this.db.getRepository<CollectionRepository>('collections').setApp(this.app);
|
this.db.getRepository<CollectionRepository>('collections').setApp(this.app);
|
||||||
|
|
||||||
const errorHandlerPlugin = this.app.getPlugin<PluginErrorHandler>('error-handler');
|
const errorHandlerPlugin = this.app.getPlugin<PluginErrorHandler>('error-handler');
|
||||||
|
@ -13,6 +13,7 @@ export default defineCollection({
|
|||||||
name: 'dataSourcesCollections',
|
name: 'dataSourcesCollections',
|
||||||
model: 'DataSourcesCollectionModel',
|
model: 'DataSourcesCollectionModel',
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
shared: true,
|
shared: true,
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
|
@ -13,6 +13,7 @@ export default defineCollection({
|
|||||||
name: 'dataSourcesFields',
|
name: 'dataSourcesFields',
|
||||||
model: 'DataSourcesFieldModel',
|
model: 'DataSourcesFieldModel',
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
shared: true,
|
shared: true,
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
|
@ -11,6 +11,7 @@ import { defineCollection } from '@nocobase/database';
|
|||||||
|
|
||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
name: 'dataSourcesRolesResourcesActions',
|
name: 'dataSourcesRolesResourcesActions',
|
||||||
model: 'DataSourcesRolesResourcesActionModel',
|
model: 'DataSourcesRolesResourcesActionModel',
|
||||||
fields: [
|
fields: [
|
||||||
|
@ -11,6 +11,7 @@ import { defineCollection } from '@nocobase/database';
|
|||||||
|
|
||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
name: 'dataSourcesRolesResourcesScopes',
|
name: 'dataSourcesRolesResourcesScopes',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
@ -11,6 +11,7 @@ import { defineCollection } from '@nocobase/database';
|
|||||||
|
|
||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
name: 'dataSourcesRolesResources',
|
name: 'dataSourcesRolesResources',
|
||||||
model: 'DataSourcesRolesResourcesModel',
|
model: 'DataSourcesRolesResourcesModel',
|
||||||
fields: [
|
fields: [
|
||||||
|
@ -12,6 +12,7 @@ import { defineCollection } from '@nocobase/database';
|
|||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
name: 'dataSourcesRoles',
|
name: 'dataSourcesRoles',
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
model: 'DataSourcesRolesModel',
|
model: 'DataSourcesRolesModel',
|
||||||
|
@ -15,6 +15,7 @@ export default defineCollection({
|
|||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
shared: true,
|
shared: true,
|
||||||
dumpRules: 'required',
|
dumpRules: 'required',
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -7,13 +7,13 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Model, Transaction } from '@nocobase/database';
|
|
||||||
import { Application } from '@nocobase/server';
|
|
||||||
import { setCurrentRole } from '@nocobase/plugin-acl';
|
|
||||||
import { ACL, AvailableActionOptions } from '@nocobase/acl';
|
import { ACL, AvailableActionOptions } from '@nocobase/acl';
|
||||||
import { DataSourcesRolesModel } from './data-sources-roles-model';
|
import { Model, Transaction } from '@nocobase/database';
|
||||||
|
import { setCurrentRole } from '@nocobase/plugin-acl';
|
||||||
|
import { Application } from '@nocobase/server';
|
||||||
|
import path from 'path';
|
||||||
import PluginDataSourceManagerServer from '../plugin';
|
import PluginDataSourceManagerServer from '../plugin';
|
||||||
import * as path from 'path';
|
import { DataSourcesRolesModel } from './data-sources-roles-model';
|
||||||
|
|
||||||
const availableActions: {
|
const availableActions: {
|
||||||
[key: string]: AvailableActionOptions;
|
[key: string]: AvailableActionOptions;
|
||||||
|
@ -17,12 +17,12 @@ import rolesConnectionResourcesResourcer from './resourcers/data-sources-resourc
|
|||||||
import databaseConnectionsRolesResourcer from './resourcers/data-sources-roles';
|
import databaseConnectionsRolesResourcer from './resourcers/data-sources-roles';
|
||||||
import { rolesRemoteCollectionsResourcer } from './resourcers/roles-data-sources-collections';
|
import { rolesRemoteCollectionsResourcer } from './resourcers/roles-data-sources-collections';
|
||||||
|
|
||||||
|
import { LoadingProgress } from '@nocobase/data-source-manager';
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import { DataSourcesRolesResourcesModel } from './models/connections-roles-resources';
|
import { DataSourcesRolesResourcesModel } from './models/connections-roles-resources';
|
||||||
import { DataSourcesRolesResourcesActionModel } from './models/connections-roles-resources-action';
|
import { DataSourcesRolesResourcesActionModel } from './models/connections-roles-resources-action';
|
||||||
import { DataSourceModel } from './models/data-source';
|
import { DataSourceModel } from './models/data-source';
|
||||||
import { DataSourcesRolesModel } from './models/data-sources-roles-model';
|
import { DataSourcesRolesModel } from './models/data-sources-roles-model';
|
||||||
import { LoadingProgress } from '@nocobase/data-source-manager';
|
|
||||||
|
|
||||||
type DataSourceState = 'loading' | 'loaded' | 'loading-failed' | 'reloading' | 'reloading-failed';
|
type DataSourceState = 'loading' | 'loaded' | 'loading-failed' | 'reloading' | 'reloading-failed';
|
||||||
|
|
||||||
@ -131,6 +131,10 @@ export class PluginDataSourceManagerServer extends Plugin {
|
|||||||
[dataSourceKey: string]: LoadingProgress;
|
[dataSourceKey: string]: LoadingProgress;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
renderJsonTemplate(template) {
|
||||||
|
return this.app.environment.renderJsonTemplate(template);
|
||||||
|
}
|
||||||
|
|
||||||
async beforeLoad() {
|
async beforeLoad() {
|
||||||
this.app.db.registerModels({
|
this.app.db.registerModels({
|
||||||
DataSourcesCollectionModel,
|
DataSourcesCollectionModel,
|
||||||
@ -187,7 +191,7 @@ export class PluginDataSourceManagerServer extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await klass.testConnection(dataSourceOptions);
|
await klass.testConnection(this.renderJsonTemplate(dataSourceOptions || {}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Test connection failed: ${error.message}`);
|
throw new Error(`Test connection failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
@ -398,7 +402,7 @@ export class PluginDataSourceManagerServer extends Plugin {
|
|||||||
const klass = ctx.app.dataSourceManager.factory.getClass(type);
|
const klass = ctx.app.dataSourceManager.factory.getClass(type);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await klass.testConnection(options);
|
await klass.testConnection(self.renderJsonTemplate(options));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Test connection failed: ${error.message}`);
|
throw new Error(`Test connection failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/src
|
@ -0,0 +1 @@
|
|||||||
|
# @nocobase/plugin-environment-variables
|
2
packages/plugins/@nocobase/plugin-environment-variables/client.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-environment-variables/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/client';
|
||||||
|
export { default } from './dist/client';
|
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/client/index.js');
|
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "@nocobase/plugin-environment-variables",
|
||||||
|
"version": "1.6.0-alpha.9",
|
||||||
|
"main": "dist/server/index.js",
|
||||||
|
"dependencies": {},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nocobase/client": "1.x",
|
||||||
|
"@nocobase/server": "1.x",
|
||||||
|
"@nocobase/test": "1.x"
|
||||||
|
},
|
||||||
|
"displayName": "Environment variables",
|
||||||
|
"displayName.zh-CN": "环境变量",
|
||||||
|
"description": "Centralized configuration and management of environment variables, used for sensitive data storage, configuration data reuse, multi-environment isolation, and more.",
|
||||||
|
"description.zh-CN": "集中配置和管理环境变量,用于敏感数据存储、配置数据重用、多环境隔离等。",
|
||||||
|
"keywords": [
|
||||||
|
"System management"
|
||||||
|
]
|
||||||
|
}
|
2
packages/plugins/@nocobase/plugin-environment-variables/server.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-environment-variables/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/server';
|
||||||
|
export { default } from './dist/server';
|
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/server/index.js');
|
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { observer } from '@formily/react';
|
||||||
|
import { useIsAdminPage, useRequest } from '@nocobase/client';
|
||||||
|
import React, { createContext } from 'react';
|
||||||
|
|
||||||
|
const EnvAndSecretsContext = createContext<any>({});
|
||||||
|
|
||||||
|
const InternalProvider = (props) => {
|
||||||
|
const variablesRequest = useRequest<any>({
|
||||||
|
url: 'environmentVariables?paginate=false',
|
||||||
|
});
|
||||||
|
return <EnvAndSecretsContext.Provider value={{ variablesRequest }}>{props.children}</EnvAndSecretsContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EnvironmentVariablesAndSecretsProvider = observer(
|
||||||
|
(props) => {
|
||||||
|
const isAdminPage = useIsAdminPage();
|
||||||
|
if (!isAdminPage) {
|
||||||
|
return <>{props.children}</>;
|
||||||
|
}
|
||||||
|
return <InternalProvider {...props} />;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'EnvironmentVariablesAndSecretsProvider',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export { EnvAndSecretsContext, EnvironmentVariablesAndSecretsProvider };
|
249
packages/plugins/@nocobase/plugin-environment-variables/src/client/client.d.ts
vendored
Normal file
249
packages/plugins/@nocobase/plugin-environment-variables/src/client/client.d.ts
vendored
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// CSS modules
|
||||||
|
type CSSModuleClasses = { readonly [key: string]: string };
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.sass' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.less' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.styl' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.stylus' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.pcss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.sss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS
|
||||||
|
declare module '*.css' { }
|
||||||
|
declare module '*.scss' { }
|
||||||
|
declare module '*.sass' { }
|
||||||
|
declare module '*.less' { }
|
||||||
|
declare module '*.styl' { }
|
||||||
|
declare module '*.stylus' { }
|
||||||
|
declare module '*.pcss' { }
|
||||||
|
declare module '*.sss' { }
|
||||||
|
|
||||||
|
// Built-in asset types
|
||||||
|
// see `src/node/constants.ts`
|
||||||
|
|
||||||
|
// images
|
||||||
|
declare module '*.apng' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.png' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jfif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pjpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pjp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.gif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.svg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ico' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.webp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.avif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// media
|
||||||
|
declare module '*.mp4' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.webm' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ogg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.mp3' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.wav' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.flac' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.aac' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.opus' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.mov' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.m4a' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.vtt' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fonts
|
||||||
|
declare module '*.woff' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.woff2' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.eot' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ttf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.otf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// other
|
||||||
|
declare module '*.webmanifest' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pdf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.txt' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wasm?init
|
||||||
|
declare module '*.wasm?init' {
|
||||||
|
const initWasm: (options?: WebAssembly.Imports) => Promise<WebAssembly.Instance>;
|
||||||
|
export default initWasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// web worker
|
||||||
|
declare module '*?worker' {
|
||||||
|
const workerConstructor: {
|
||||||
|
new(options?: { name?: string }): Worker;
|
||||||
|
};
|
||||||
|
export default workerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?worker&inline' {
|
||||||
|
const workerConstructor: {
|
||||||
|
new(options?: { name?: string }): Worker;
|
||||||
|
};
|
||||||
|
export default workerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?worker&url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker' {
|
||||||
|
const sharedWorkerConstructor: {
|
||||||
|
new(options?: { name?: string }): SharedWorker;
|
||||||
|
};
|
||||||
|
export default sharedWorkerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker&inline' {
|
||||||
|
const sharedWorkerConstructor: {
|
||||||
|
new(options?: { name?: string }): SharedWorker;
|
||||||
|
};
|
||||||
|
export default sharedWorkerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker&url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?raw' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?inline' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { EnvironmentTabs } from './EnvironmentTabs';
|
||||||
|
|
||||||
|
export default function EnvironmentPage() {
|
||||||
|
return <EnvironmentTabs />;
|
||||||
|
}
|
@ -0,0 +1,510 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
import { DownOutlined, PlusOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
Checkbox,
|
||||||
|
FormButtonGroup,
|
||||||
|
FormDrawer,
|
||||||
|
FormItem,
|
||||||
|
FormLayout,
|
||||||
|
Input,
|
||||||
|
Radio,
|
||||||
|
Reset,
|
||||||
|
Submit,
|
||||||
|
} from '@formily/antd-v5';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { registerValidateRules } from '@formily/core';
|
||||||
|
import { createSchemaField } from '@formily/react';
|
||||||
|
import {
|
||||||
|
SchemaComponentOptions,
|
||||||
|
useAPIClient,
|
||||||
|
SchemaComponent,
|
||||||
|
useFilterFieldProps,
|
||||||
|
useFilterFieldOptions,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { Alert, App, Button, Card, Dropdown, Flex, Space, Table, Tag } from 'antd';
|
||||||
|
import React, { useContext, useState } from 'react';
|
||||||
|
import { VAR_NAME_RE } from '../../re';
|
||||||
|
import { EnvAndSecretsContext } from '../EnvironmentVariablesAndSecretsProvider';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
|
||||||
|
registerValidateRules({
|
||||||
|
env_name_rule(value) {
|
||||||
|
if (!value) return '';
|
||||||
|
return VAR_NAME_RE.test(value)
|
||||||
|
? ''
|
||||||
|
: 'Only letters, numbers and underscores are allowed, and it must start with a letter.';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const SchemaField = createSchemaField({
|
||||||
|
components: {
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
Checkbox,
|
||||||
|
Radio,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bulkSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
variables: {
|
||||||
|
type: 'string',
|
||||||
|
title: `{{ t("Plain text") }}`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.TextArea',
|
||||||
|
'x-component-props': {
|
||||||
|
autoSize: { minRows: 10, maxRows: 20 },
|
||||||
|
placeholder: `FOO=aaa
|
||||||
|
BAR=bbb
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secret: {
|
||||||
|
type: 'string',
|
||||||
|
title: `{{ t("Encrypted") }}`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.TextArea',
|
||||||
|
'x-component-props': {
|
||||||
|
autoSize: { minRows: 10, maxRows: 20 },
|
||||||
|
placeholder: `FOO=aaa
|
||||||
|
BAR=bbb
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
title: `{{ t("Name") }}`,
|
||||||
|
required: true,
|
||||||
|
'x-validator': {
|
||||||
|
env_name_rule: true,
|
||||||
|
},
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-disabled': '{{ !createOnly }}',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
title: `{{ t("Type") }}`,
|
||||||
|
required: true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Radio.Group',
|
||||||
|
default: 'default',
|
||||||
|
enum: [
|
||||||
|
{
|
||||||
|
value: 'default',
|
||||||
|
label: '{{t("Plain text")}}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'secret',
|
||||||
|
label: '{{t("Encrypted")}}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: 'string',
|
||||||
|
title: `{{ t("Value") }}`,
|
||||||
|
required: true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.TextArea',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EnvironmentVariables({ request, setSelectRowKeys }) {
|
||||||
|
const { modal } = App.useApp();
|
||||||
|
const t = useT();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const { data, loading, refresh } = request || {};
|
||||||
|
|
||||||
|
const typEnum = {
|
||||||
|
default: {
|
||||||
|
label: t('Plain text'),
|
||||||
|
color: 'green',
|
||||||
|
},
|
||||||
|
secret: {
|
||||||
|
label: t('Encrypted'),
|
||||||
|
color: 'red',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const resource = api.resource('environmentVariables');
|
||||||
|
|
||||||
|
const handleDelete = (data) => {
|
||||||
|
modal.confirm({
|
||||||
|
title: t('Delete variable'),
|
||||||
|
content: t('Are you sure you want to delete it?'),
|
||||||
|
async onOk() {
|
||||||
|
await resource.destroy({
|
||||||
|
filterByTk: data.name,
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = async (initialValues) => {
|
||||||
|
const drawer = FormDrawer({ title: t('Edit') }, () => {
|
||||||
|
return (
|
||||||
|
<FormLayout layout={'vertical'}>
|
||||||
|
<SchemaComponentOptions scope={{ createOnly: false, t }}>
|
||||||
|
<SchemaField schema={schema} />
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
<FormDrawer.Footer>
|
||||||
|
<FormButtonGroup align="right">
|
||||||
|
<Reset
|
||||||
|
onClick={() => {
|
||||||
|
drawer.close();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Reset>
|
||||||
|
<Submit
|
||||||
|
onSubmit={async (data) => {
|
||||||
|
await api.request({
|
||||||
|
url: `environmentVariables:update?filterByTk=${initialValues.name}`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
request.refresh();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Submit')}
|
||||||
|
</Submit>
|
||||||
|
</FormButtonGroup>
|
||||||
|
</FormDrawer.Footer>
|
||||||
|
</FormLayout>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
drawer.open({
|
||||||
|
initialValues: { ...initialValues },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
size="middle"
|
||||||
|
rowKey={'name'}
|
||||||
|
dataSource={data?.data}
|
||||||
|
pagination={false}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: t('Name'),
|
||||||
|
dataIndex: 'name',
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Type'),
|
||||||
|
dataIndex: 'type',
|
||||||
|
render: (value) => <Tag color={typEnum[value].color}>{typEnum[value].label}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Value'),
|
||||||
|
ellipsis: true,
|
||||||
|
render: (record) => <div>{record.type === 'default' ? record.value : '******'}</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Actions'),
|
||||||
|
width: 200,
|
||||||
|
render: (record) => (
|
||||||
|
<Space>
|
||||||
|
<a onClick={() => handleEdit(record)}>{t('Edit')}</a>
|
||||||
|
<a onClick={() => handleDelete(record)}>{t('Delete')}</a>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
rowSelection={{
|
||||||
|
type: 'checkbox',
|
||||||
|
onChange: (rowKeys, selectedRows) => {
|
||||||
|
setSelectRowKeys(rowKeys);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} input - The input string containing key-value pairs, separated by `=` and line breaks.
|
||||||
|
* @returns {Array<{name: string, value: string}>} - The converted array of objects.
|
||||||
|
*/
|
||||||
|
function parseKeyValuePairs(input, type) {
|
||||||
|
return input
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => {
|
||||||
|
const [name, ...valueParts] = line.split('='); // 按 `=` 分割
|
||||||
|
return (
|
||||||
|
name && {
|
||||||
|
name: name.trim(),
|
||||||
|
value: valueParts.join('=').trim(),
|
||||||
|
type: type === 'secret' ? 'secret' : 'default',
|
||||||
|
}
|
||||||
|
); // 去除多余空格
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EnvironmentTabs() {
|
||||||
|
const api = useAPIClient();
|
||||||
|
const t = useT();
|
||||||
|
const { modal } = App.useApp();
|
||||||
|
const { variablesRequest } = useContext(EnvAndSecretsContext);
|
||||||
|
const [selectRowKeys, setSelectRowKeys] = useState([]);
|
||||||
|
const resource = api.resource('environmentVariables');
|
||||||
|
|
||||||
|
const handleBulkImport = async (importData) => {
|
||||||
|
const arr = Object.entries(importData).map(([type, dataString]) => {
|
||||||
|
return parseKeyValuePairs(dataString, type).filter(Boolean);
|
||||||
|
});
|
||||||
|
await api.request({
|
||||||
|
url: 'environmentVariables:create',
|
||||||
|
method: 'post',
|
||||||
|
data: arr.flat(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handelBulkDelete = () => {
|
||||||
|
if (selectRowKeys.length > 0) {
|
||||||
|
modal.confirm({
|
||||||
|
title: t('Delete variable'),
|
||||||
|
content: t('Are you sure you want to delete it?'),
|
||||||
|
async onOk() {
|
||||||
|
await resource.destroy({
|
||||||
|
filterByTk: selectRowKeys,
|
||||||
|
});
|
||||||
|
variablesRequest?.refresh?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handelRefresh = () => {
|
||||||
|
variablesRequest?.refresh?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterOptions = [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
title: t('Name'),
|
||||||
|
operators: [
|
||||||
|
{ label: '{{t("contains")}}', value: '$includes', selected: true },
|
||||||
|
{ label: '{{t("does not contain")}}', value: '$notIncludes' },
|
||||||
|
{ label: '{{t("is")}}', value: '$eq' },
|
||||||
|
{ label: '{{t("is not")}}', value: '$ne' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('Name'),
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
title: t('Type'),
|
||||||
|
operators: [
|
||||||
|
{
|
||||||
|
label: '{{t("is")}}',
|
||||||
|
value: '$match',
|
||||||
|
selected: true,
|
||||||
|
schema: {
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-component-props': { mode: 'tags' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '{{t("is not")}}',
|
||||||
|
value: '$notMatch',
|
||||||
|
schema: {
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-component-props': { mode: 'tags' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('Type'),
|
||||||
|
'x-component': 'Select',
|
||||||
|
enum: [
|
||||||
|
{
|
||||||
|
value: 'default',
|
||||||
|
label: '{{t("Plain text")}}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'secret',
|
||||||
|
label: '{{t("Encrypted")}}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
title: t('Value'),
|
||||||
|
operators: [
|
||||||
|
{ label: '{{t("contains")}}', value: '$includes', selected: true },
|
||||||
|
{ label: '{{t("does not contain")}}', value: '$notIncludes' },
|
||||||
|
{ label: '{{t("is")}}', value: '$eq' },
|
||||||
|
{ label: '{{t("is not")}}', value: '$ne' },
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
title: t('Value'),
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const useFilterActionProps = () => {
|
||||||
|
const field = useField<any>();
|
||||||
|
const { run } = variablesRequest;
|
||||||
|
|
||||||
|
return {
|
||||||
|
options: filterOptions,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
run(values);
|
||||||
|
|
||||||
|
field.setValue(values);
|
||||||
|
},
|
||||||
|
onReset: (values) => {
|
||||||
|
field.setValue(values);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{variablesRequest?.data?.meta?.updated && (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
style={{ marginBottom: '1.2em', alignItems: 'center' }}
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
{t('Environment variables have been updated. A restart is required for the changes to take effect.')}{' '}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
size="middle"
|
||||||
|
type="primary"
|
||||||
|
onClick={async () => {
|
||||||
|
await api.resource('app').refresh();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Restart now')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Card>
|
||||||
|
<div style={{ float: 'left' }}>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
name: 'filter',
|
||||||
|
type: 'object',
|
||||||
|
title: '{{ t("Filter") }}',
|
||||||
|
default: {
|
||||||
|
$and: [{ name: { $includes: '' } }],
|
||||||
|
},
|
||||||
|
'x-component': 'Filter.Action',
|
||||||
|
|
||||||
|
enum: filterOptions,
|
||||||
|
'x-use-component-props': useFilterActionProps,
|
||||||
|
}}
|
||||||
|
scope={{ t }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Flex justify="end" style={{ marginBottom: 16 }}>
|
||||||
|
<Space>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={handelRefresh}>
|
||||||
|
{t('Refresh')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button icon={<DeleteOutlined />} onClick={handelBulkDelete}>
|
||||||
|
{t('Delete')}
|
||||||
|
</Button>
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
onClick(info) {
|
||||||
|
FormDrawer(
|
||||||
|
{
|
||||||
|
variable: t('Add variable'),
|
||||||
|
bulk: t('Bulk import'),
|
||||||
|
}[info.key],
|
||||||
|
() => {
|
||||||
|
return (
|
||||||
|
<FormLayout layout={'vertical'}>
|
||||||
|
<SchemaComponentOptions scope={{ createOnly: true, t }}>
|
||||||
|
<SchemaField schema={info.key === 'bulk' ? bulkSchema : schema} />
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
<FormDrawer.Footer>
|
||||||
|
<FormButtonGroup align="right">
|
||||||
|
<Reset>{t('Cancel')}</Reset>
|
||||||
|
<Submit
|
||||||
|
onSubmit={async (data) => {
|
||||||
|
if (info.key === 'bulk') {
|
||||||
|
await handleBulkImport(data);
|
||||||
|
variablesRequest.refresh();
|
||||||
|
} else {
|
||||||
|
await api.request({
|
||||||
|
url: 'environmentVariables:create',
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
variablesRequest.refresh();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Submit')}
|
||||||
|
</Submit>
|
||||||
|
</FormButtonGroup>
|
||||||
|
</FormDrawer.Footer>
|
||||||
|
</FormLayout>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.open({
|
||||||
|
initialValues: {},
|
||||||
|
})
|
||||||
|
.then(console.log)
|
||||||
|
.catch(console.log);
|
||||||
|
},
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'variable',
|
||||||
|
label: t('Add variable'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bulk',
|
||||||
|
label: t('Bulk import'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="primary" icon={<PlusOutlined />}>
|
||||||
|
{t('Add new')} <DownOutlined />
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
<EnvironmentVariables request={variablesRequest} setSelectRowKeys={setSelectRowKeys} />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import { EnvironmentVariablesAndSecretsProvider } from './EnvironmentVariablesAndSecretsProvider';
|
||||||
|
import EnvironmentPage from './components/EnvironmentPage';
|
||||||
|
import { useGetEnvironmentVariables } from './utils';
|
||||||
|
|
||||||
|
export class PluginEnvironmentVariablesClient extends Plugin {
|
||||||
|
async load() {
|
||||||
|
this.app.pluginSettingsManager.add('environment', {
|
||||||
|
title: this.t('Environment variables'),
|
||||||
|
icon: 'TableOutlined',
|
||||||
|
Component: EnvironmentPage,
|
||||||
|
});
|
||||||
|
this.app.addGlobalVar('$env', useGetEnvironmentVariables);
|
||||||
|
this.app.use(EnvironmentVariablesAndSecretsProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginEnvironmentVariablesClient;
|
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import pkg from './../../package.json';
|
||||||
|
import { useApp } from '@nocobase/client';
|
||||||
|
|
||||||
|
export function useT() {
|
||||||
|
const app = useApp();
|
||||||
|
return (str: string) => app.i18n.t(str, { ns: [pkg.name, 'client'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tStr(key: string) {
|
||||||
|
return `{{t(${JSON.stringify(key)}, { ns: ['${pkg.name}', 'client'], nsMode: 'fallback' })}}`;
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { EnvAndSecretsContext } from './EnvironmentVariablesAndSecretsProvider';
|
||||||
|
import { useT } from './locale';
|
||||||
|
|
||||||
|
export const useGetEnvironmentVariables = () => {
|
||||||
|
const t = useT();
|
||||||
|
const { variablesRequest } = useContext(EnvAndSecretsContext);
|
||||||
|
const { data: variables, loading: variablesLoading } = variablesRequest || {};
|
||||||
|
if (!variablesLoading && variables?.data?.length) {
|
||||||
|
return {
|
||||||
|
name: '$env',
|
||||||
|
title: t('Environment variables'),
|
||||||
|
value: '$env',
|
||||||
|
label: t('Environment variables'),
|
||||||
|
children: variables?.data
|
||||||
|
.map((v) => {
|
||||||
|
return { title: v.name, name: v.name, value: v.name, label: v.name };
|
||||||
|
})
|
||||||
|
.filter(Boolean),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './server';
|
||||||
|
export { default } from './server';
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"Environment": "环境",
|
||||||
|
"Environment variables": "环境变量",
|
||||||
|
"Variables": "变量",
|
||||||
|
"Secrets": "密钥",
|
||||||
|
"Add variable": "添加变量",
|
||||||
|
"Bulk import": "批量导入",
|
||||||
|
"Name": "名称",
|
||||||
|
"Value": "值",
|
||||||
|
"Type": "类型",
|
||||||
|
"Plain text": "明文",
|
||||||
|
"Encrypted": "加密",
|
||||||
|
"Delete variable": "删除变量",
|
||||||
|
"Restart now": "立即重启",
|
||||||
|
"Environment variables have been updated. A restart is required for the changes to take effect.": "检测到环境变量有更新,需重启应用才能生效。"
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const VAR_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
class AesEncryptor {
|
||||||
|
private key: Buffer;
|
||||||
|
|
||||||
|
constructor(key: Buffer) {
|
||||||
|
if (key.length !== 32) {
|
||||||
|
throw new Error('Key must be 32 bytes for AES-256 encryption.');
|
||||||
|
}
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encrypt(text: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-cbc', this.key as any, iv as any);
|
||||||
|
|
||||||
|
const encrypted = Buffer.concat([cipher.update(Buffer.from(text, 'utf8') as any), cipher.final()] as any[]);
|
||||||
|
|
||||||
|
resolve(iv.toString('hex') + encrypted.toString('hex'));
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(encryptedText: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const iv = Buffer.from(encryptedText.slice(0, 32), 'hex'); // 提取前 16 字节作为 IV
|
||||||
|
const encrypted = Buffer.from(encryptedText.slice(32), 'hex'); // 提取密文
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-cbc', this.key as any, iv as any);
|
||||||
|
|
||||||
|
const decrypted = Buffer.concat([decipher.update(encrypted as any), decipher.final()] as any);
|
||||||
|
|
||||||
|
resolve(decrypted.toString('utf8'));
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getOrGenerateKey(keyFilePath: string): Promise<Buffer> {
|
||||||
|
try {
|
||||||
|
const key = await fs.readFile(keyFilePath);
|
||||||
|
if (key.length !== 32) {
|
||||||
|
throw new Error('Invalid key length in file.');
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
const key = crypto.randomBytes(32);
|
||||||
|
await fs.mkdir(path.dirname(keyFilePath), { recursive: true });
|
||||||
|
await fs.writeFile(keyFilePath, key as any);
|
||||||
|
return key;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Failed to load key: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AesEncryptor;
|
@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineCollection } from '@nocobase/database';
|
||||||
|
import { VAR_NAME_RE } from '../../re';
|
||||||
|
|
||||||
|
export default defineCollection({
|
||||||
|
name: 'environmentVariables',
|
||||||
|
autoGenId: false,
|
||||||
|
migrationRules: ['schema-only'],
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
primaryKey: true,
|
||||||
|
validate: {
|
||||||
|
is: VAR_NAME_RE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'type',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default } from './plugin';
|
@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Plugin } from '@nocobase/server';
|
||||||
|
import path from 'path';
|
||||||
|
import AesEncryptor from './AesEncryptor';
|
||||||
|
|
||||||
|
export class PluginEnvironmentVariablesServer extends Plugin {
|
||||||
|
aesEncryptor: AesEncryptor;
|
||||||
|
updated = false;
|
||||||
|
async handleSyncMessage(message) {
|
||||||
|
const { type, name, value } = message;
|
||||||
|
if (type === 'updated') {
|
||||||
|
this.updated = true;
|
||||||
|
} else if (type === 'setVariable') {
|
||||||
|
this.app.environment.setVariable(name, value);
|
||||||
|
} else if (type === 'removeVariable') {
|
||||||
|
this.app.environment.removeVariable(name);
|
||||||
|
this.updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.createAesEncryptor();
|
||||||
|
this.registerACL();
|
||||||
|
this.onEnvironmentSaved();
|
||||||
|
await this.loadVariables();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAesEncryptor() {
|
||||||
|
const key = await AesEncryptor.getOrGenerateKey(
|
||||||
|
path.resolve(process.cwd(), 'storage', this.name, this.app.name, 'aes_key.dat'),
|
||||||
|
);
|
||||||
|
this.aesEncryptor = new AesEncryptor(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerACL() {
|
||||||
|
this.app.acl.allow('environmentVariables', 'list', 'loggedIn');
|
||||||
|
this.app.acl.registerSnippet({
|
||||||
|
name: `pm.${this.name}`,
|
||||||
|
actions: ['environmentVariables:*', 'app:refresh'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async listEnvironmentVariables() {
|
||||||
|
const repository = this.db.getRepository('environmentVariables');
|
||||||
|
const items = await repository.find({
|
||||||
|
sort: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
return items.map(({ name, type }) => ({ name, type }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async setEnvironmentVariablesByText(texts: Array<{ text: string; secret: boolean }>) {
|
||||||
|
/*
|
||||||
|
text format:
|
||||||
|
KEY1=VALUE1
|
||||||
|
KEY2=VALUE2
|
||||||
|
# This is a comment
|
||||||
|
KEY3=VALUE3
|
||||||
|
*/
|
||||||
|
const repository = this.db.getRepository('environmentVariables');
|
||||||
|
|
||||||
|
for (const { text, secret } of texts) {
|
||||||
|
// Split by newline and process each line
|
||||||
|
const lines = text
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim()) // Remove leading/trailing spaces
|
||||||
|
.filter((line) => line && !line.startsWith('#')); // Remove empty lines and comments
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Find first '=' to support values containing '='
|
||||||
|
const equalIndex = line.indexOf('=');
|
||||||
|
if (equalIndex === -1) {
|
||||||
|
this.app.log.warn(`Invalid environment variable format: ${line}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = line.slice(0, equalIndex).trim();
|
||||||
|
const value = line.slice(equalIndex + 1).trim();
|
||||||
|
|
||||||
|
if (!key || !value) {
|
||||||
|
this.app.log.warn(`Empty key or value found: ${line}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await repository.create({
|
||||||
|
values: {
|
||||||
|
name: key,
|
||||||
|
type: secret ? 'secret' : 'default',
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnvironmentSaved() {
|
||||||
|
this.db.on('environmentVariables.afterUpdate', async (model, { transaction }) => {
|
||||||
|
this.updated = true;
|
||||||
|
this.sendSyncMessage({ type: 'updated' }, { transaction });
|
||||||
|
});
|
||||||
|
this.db.on('environmentVariables.beforeSave', async (model) => {
|
||||||
|
if (model.type === 'secret' && model.changed('value')) {
|
||||||
|
const encrypted = await this.aesEncryptor.encrypt(model.value);
|
||||||
|
model.set('value', encrypted);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.app.resourceManager.registerActionHandler('environmentVariables:list', async (ctx, next) => {
|
||||||
|
const repository = this.db.getRepository('environmentVariables');
|
||||||
|
const items = await repository.find({
|
||||||
|
sort: 'name',
|
||||||
|
filter: ctx.action.params.filter,
|
||||||
|
});
|
||||||
|
for (const model of items) {
|
||||||
|
if (model.type === 'secret') {
|
||||||
|
model.set('value', undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.withoutDataWrapping = true;
|
||||||
|
ctx.body = {
|
||||||
|
data: items,
|
||||||
|
meta: {
|
||||||
|
updated: this.updated,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
this.db.on('environmentVariables.afterSave', async (model, { transaction }) => {
|
||||||
|
if (model.type === 'secret') {
|
||||||
|
try {
|
||||||
|
const decrypted = await this.aesEncryptor.decrypt(model.value);
|
||||||
|
model.set('value', decrypted);
|
||||||
|
} catch (error) {
|
||||||
|
this.app.log.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.app.environment.setVariable(model.name, model.value);
|
||||||
|
this.sendSyncMessage({ type: 'setVariable', name: model.name, value: model.value }, { transaction });
|
||||||
|
});
|
||||||
|
this.db.on('environmentVariables.afterDestroy', async (model, { transaction }) => {
|
||||||
|
this.app.environment.removeVariable(model.name);
|
||||||
|
this.updated = true;
|
||||||
|
this.sendSyncMessage({ type: 'removeVariable', name: model.name }, { transaction });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadVariables() {
|
||||||
|
const repository = this.db.getRepository('environmentVariables');
|
||||||
|
const r = await repository.collection.existsInDb();
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items = await repository.find();
|
||||||
|
for (const model of items) {
|
||||||
|
if (model.type === 'secret') {
|
||||||
|
try {
|
||||||
|
const decrypted = await this.aesEncryptor.decrypt(model.value);
|
||||||
|
model.set('value', decrypted);
|
||||||
|
} catch (error) {
|
||||||
|
this.app.log.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.app.environment.setVariable(model.name, model.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginEnvironmentVariablesServer;
|
@ -11,6 +11,7 @@ import { defineCollection } from '@nocobase/database';
|
|||||||
|
|
||||||
export default defineCollection({
|
export default defineCollection({
|
||||||
dumpRules: 'skipped',
|
dumpRules: 'skipped',
|
||||||
|
migrationRules: ['schema-only', 'overwrite', 'skip'],
|
||||||
name: 'chinaRegions',
|
name: 'chinaRegions',
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
fields: [
|
fields: [
|
||||||
|
@ -65,6 +65,7 @@ export default defineCollection({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
migrationRules: ['overwrite', 'skip'],
|
||||||
name: 'sequences',
|
name: 'sequences',
|
||||||
shared: true,
|
shared: true,
|
||||||
fields: [
|
fields: [
|
||||||
|
@ -31,35 +31,36 @@ const schema = {
|
|||||||
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
accessKeyId: {
|
accessKeyId: {
|
||||||
title: `{{t("AccessKey ID", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("AccessKey ID", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
accessKeySecret: {
|
accessKeySecret: {
|
||||||
title: `{{t("AccessKey Secret", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("AccessKey Secret", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Password',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
|
'x-component-props': { password: true },
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
bucket: {
|
bucket: {
|
||||||
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
thumbnailRule: {
|
thumbnailRule: {
|
||||||
title: 'Thumbnail rule',
|
title: 'Thumbnail rule',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -69,14 +70,14 @@ const schema = {
|
|||||||
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
SecretId: {
|
SecretId: {
|
||||||
title: `{{t("SecretId", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("SecretId", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
SecretKey: {
|
SecretKey: {
|
||||||
@ -90,7 +91,7 @@ const schema = {
|
|||||||
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -101,35 +102,36 @@ const schema = {
|
|||||||
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
accessKeyId: {
|
accessKeyId: {
|
||||||
title: `{{t("AccessKey ID", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("AccessKey ID", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
secretAccessKey: {
|
secretAccessKey: {
|
||||||
title: `{{t("AccessKey Secret", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("AccessKey Secret", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Password',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
|
'x-component-props': { password: true },
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
bucket: {
|
bucket: {
|
||||||
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
endpoint: {
|
endpoint: {
|
||||||
title: `{{t("Endpoint", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Endpoint", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -55,7 +55,7 @@ const collection = {
|
|||||||
uiSchema: {
|
uiSchema: {
|
||||||
title: `{{t("Access base URL", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Access base URL", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
} as ISchema,
|
} as ISchema,
|
||||||
},
|
},
|
||||||
@ -66,7 +66,7 @@ const collection = {
|
|||||||
uiSchema: {
|
uiSchema: {
|
||||||
title: `{{t("Path", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Path", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
} as ISchema,
|
} as ISchema,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -28,7 +28,7 @@ export default {
|
|||||||
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
description: `{{t('Aliyun OSS region part of the bucket. For example: "oss-cn-beijing".', { ns: "${NAMESPACE}" })}}`,
|
description: `{{t('Aliyun OSS region part of the bucket. For example: "oss-cn-beijing".', { ns: "${NAMESPACE}" })}}`,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@ -36,28 +36,29 @@ export default {
|
|||||||
title: `{{t("AccessKey ID", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("AccessKey ID", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
accessKeySecret: {
|
accessKeySecret: {
|
||||||
title: `{{t("AccessKey Secret", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("AccessKey Secret", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Password',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
|
'x-component-props': { password: true },
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
bucket: {
|
bucket: {
|
||||||
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
thumbnailRule: {
|
thumbnailRule: {
|
||||||
title: 'Thumbnail rule',
|
title: 'Thumbnail rule',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
placeholder: '?x-oss-process=image/auto-orient,1/resize,m_fill,w_94,h_94/quality,q_90',
|
placeholder: '?x-oss-process=image/auto-orient,1/resize,m_fill,w_94,h_94/quality,q_90',
|
||||||
},
|
},
|
||||||
|
@ -25,35 +25,36 @@ export default {
|
|||||||
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
accessKeyId: {
|
accessKeyId: {
|
||||||
title: `{{t("AccessKey ID", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("AccessKey ID", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
secretAccessKey: {
|
secretAccessKey: {
|
||||||
title: `{{t("AccessKey Secret", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("AccessKey Secret", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Password',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
|
'x-component-props': { password: true },
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
bucket: {
|
bucket: {
|
||||||
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
endpoint: {
|
endpoint: {
|
||||||
title: `{{t("Endpoint", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Endpoint", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -27,35 +27,38 @@ export default {
|
|||||||
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Region", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
SecretId: {
|
SecretId: {
|
||||||
title: `{{t("SecretId", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("SecretId", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
SecretKey: {
|
SecretKey: {
|
||||||
title: `{{t("SecretKey", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("SecretKey", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Password',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
|
'x-component-props': {
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
Bucket: {
|
Bucket: {
|
||||||
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Bucket", { ns: "${NAMESPACE}" })}}`,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
thumbnailRule: {
|
thumbnailRule: {
|
||||||
title: 'Thumbnail rule',
|
title: 'Thumbnail rule',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
placeholder: '?imageMogr2/thumbnail/!50p',
|
placeholder: '?imageMogr2/thumbnail/!50p',
|
||||||
},
|
},
|
||||||
|
@ -94,7 +94,7 @@ export class FileCollectionTemplate extends CollectionTemplate {
|
|||||||
uiSchema: {
|
uiSchema: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: `{{t("Path", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Path", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-component': 'Input',
|
'x-component': 'TextAreaWithGlobalScope',
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -125,7 +125,9 @@ export async function createMiddleware(ctx: Context, next: Next) {
|
|||||||
const StorageRepo = ctx.db.getRepository('storages');
|
const StorageRepo = ctx.db.getRepository('storages');
|
||||||
const storage = await StorageRepo.findOne({ filter: storageName ? { name: storageName } : { default: true } });
|
const storage = await StorageRepo.findOne({ filter: storageName ? { name: storageName } : { default: true } });
|
||||||
|
|
||||||
ctx.storage = storage;
|
const plugin = ctx.app.pm.get(Plugin);
|
||||||
|
ctx.storage = plugin.parseStorage(storage);
|
||||||
|
|
||||||
if (ctx?.request.is('multipart/*')) {
|
if (ctx?.request.is('multipart/*')) {
|
||||||
await multipart(ctx, next);
|
await multipart(ctx, next);
|
||||||
} else {
|
} else {
|
||||||
@ -172,11 +174,12 @@ export async function destroyMiddleware(ctx: Context, next: Next) {
|
|||||||
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const undeleted = [];
|
const undeleted = [];
|
||||||
|
const plugin = ctx.app.pm.get(Plugin);
|
||||||
await storages.reduce(
|
await storages.reduce(
|
||||||
(promise, storage) =>
|
(promise, storage) =>
|
||||||
promise.then(async () => {
|
promise.then(async () => {
|
||||||
const storageConfig = ctx.app.pm.get(Plugin).storageTypes.get(storage.type);
|
const storageConfig = plugin.storageTypes.get(storage.type);
|
||||||
const result = await storageConfig.delete(storage, storageGroupedRecords[storage.id]);
|
const result = await storageConfig.delete(plugin.parseStorage(storage), storageGroupedRecords[storage.id]);
|
||||||
count += result[0];
|
count += result[0];
|
||||||
undeleted.push(...result[1]);
|
undeleted.push(...result[1]);
|
||||||
}),
|
}),
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user