mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
ui: new config page
This commit is contained in:
8 files changed
+179
-51
No files matched your search
@@ -183,12 +183,12 @@ class SystemConfigHandler extends SystemHandler {
|
|||||||
@requireSudo
|
@requireSudo
|
||||||
async get() {
|
async get() {
|
||||||
this.response.template = 'manage_config.html';
|
this.response.template = 'manage_config.html';
|
||||||
let value = this.ctx.config.configSource;
|
let value = this.ctx.get('config').configSource;
|
||||||
try {
|
try {
|
||||||
value = yaml.dump(Schema.intersect(this.ctx.config.settings)(yaml.load(value)));
|
value = yaml.dump(Schema.intersect(this.ctx.get('config').settings)(yaml.load(value)));
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
this.response.body = {
|
this.response.body = {
|
||||||
schema: Schema.intersect(this.ctx.config.settings).toJSON(),
|
schema: Schema.intersect(this.ctx.get('config').settings).toJSON(),
|
||||||
value,
|
value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -202,7 +202,7 @@ class SystemConfigHandler extends SystemHandler {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new ValidationError('value');
|
throw new ValidationError('value');
|
||||||
}
|
}
|
||||||
await this.ctx.config.saveConfig(config);
|
await this.ctx.get('config').saveConfig(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +321,7 @@ class SystemUserPrivHandler extends SystemHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const inject = ['config'];
|
||||||
export async function apply(ctx) {
|
export async function apply(ctx) {
|
||||||
ctx.Route('manage', '/manage', SystemMainHandler);
|
ctx.Route('manage', '/manage', SystemMainHandler);
|
||||||
ctx.Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler);
|
ctx.Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler);
|
||||||
|
|||||||
@@ -65,23 +65,23 @@ const defaultPath = process.env.CI ? '/tmp/file'
|
|||||||
const FileSetting = Schema.intersect([
|
const FileSetting = Schema.intersect([
|
||||||
Schema.object({
|
Schema.object({
|
||||||
type: Schema.union([
|
type: Schema.union([
|
||||||
Schema.const('file').description('local file provider'),
|
Schema.const('file').i18n({ en: 'Local Directory', zh: '本地目录' }),
|
||||||
Schema.const('s3').description('s3 provider'),
|
Schema.const('s3').description('S3'),
|
||||||
] as const).description('provider type'),
|
] as const).i18n({ en: 'Storage Provider Type', zh: '存储提供商类型' }),
|
||||||
endPointForUser: Schema.string().default('/fs/'),
|
endPointForUser: Schema.string().default('/fs/'),
|
||||||
endPointForJudge: Schema.string().default('/fs/'),
|
endPointForJudge: Schema.string().default('/fs/'),
|
||||||
}).description('setting_file'),
|
}).i18n({ en: 'File Storage Setting', zh: '文件存储设置' }),
|
||||||
Schema.union([
|
Schema.union([
|
||||||
Schema.object({
|
Schema.object({
|
||||||
type: Schema.const('file').required(),
|
type: Schema.const('file').required(),
|
||||||
path: Schema.string().default(defaultPath).description('Storage path'),
|
path: Schema.string().default(defaultPath).i18n({ en: 'Storage path', zh: '存储路径' }),
|
||||||
secret: Schema.string().description('Download file sign secret').default(nanoid()),
|
secret: Schema.string().default(nanoid()).i18n({ en: 'Download file sign secret', zh: '下载文件签名密钥' }),
|
||||||
}),
|
}),
|
||||||
Schema.object({
|
Schema.object({
|
||||||
type: Schema.const('s3').required(),
|
type: Schema.const('s3').required(),
|
||||||
endPoint: Schema.string(),
|
endPoint: Schema.string(),
|
||||||
accessKey: Schema.string().description('access key'),
|
accessKey: Schema.string(),
|
||||||
secretKey: Schema.string().description('secret key').role('secret'),
|
secretKey: Schema.string().role('secret'),
|
||||||
bucket: Schema.string().default('hydro'),
|
bucket: Schema.string().default('hydro'),
|
||||||
region: Schema.string().default('us-east-1'),
|
region: Schema.string().default('us-east-1'),
|
||||||
pathStyle: Schema.boolean().default(true),
|
pathStyle: Schema.boolean().default(true),
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import list from 'monaco-themes/themes/themelist.json';
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { i18n, request } from 'vj/utils';
|
import { i18n, request } from 'vj/utils';
|
||||||
|
|
||||||
|
export { renderMarkdown } from 'monaco-editor/esm/vs/base/browser/markdownRenderer';
|
||||||
|
|
||||||
export default monaco;
|
export default monaco;
|
||||||
export const customOptions: monaco.editor.IStandaloneDiffEditorConstructionOptions = JSON.parse(localStorage.getItem('editor.config') || '{}');
|
export const customOptions: monaco.editor.IStandaloneDiffEditorConstructionOptions = JSON.parse(localStorage.getItem('editor.config') || '{}');
|
||||||
export function saveCustomOptions() {
|
export function saveCustomOptions() {
|
||||||
|
|||||||
@@ -85,7 +85,12 @@ export async function load(features = ['markdown']) {
|
|||||||
}
|
}
|
||||||
await res.loadThemePromise;
|
await res.loadThemePromise;
|
||||||
resolve();
|
resolve();
|
||||||
return { monaco: res.default, registerAction: res.registerAction, customOptions: res.customOptions };
|
return {
|
||||||
|
monaco: res.default,
|
||||||
|
registerAction: res.registerAction,
|
||||||
|
customOptions: res.customOptions,
|
||||||
|
renderMarkdown: res.renderMarkdown,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default load;
|
export default load;
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class MarkdownHandler extends Handler {
|
|||||||
|
|
||||||
class SystemConfigSchemaHandler extends Handler {
|
class SystemConfigSchemaHandler extends Handler {
|
||||||
async get() {
|
async get() {
|
||||||
const schema = convert(Schema.intersect(this.ctx.config.settings) as any, true);
|
const schema = convert(Schema.intersect(this.ctx.get('config')!.settings) as any, true);
|
||||||
this.response.body = schema;
|
this.response.body = schema;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"@types/redux-logger": "^3.0.13",
|
"@types/redux-logger": "^3.0.13",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@types/webpack-env": "^1.18.8",
|
"@types/webpack-env": "^1.18.8",
|
||||||
|
"@undefined-moe/schemastery-vue": "^7.3.7",
|
||||||
"@vscode/codicons": "^0.0.36",
|
"@vscode/codicons": "^0.0.36",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"allotment": "^1.20.3",
|
"allotment": "^1.20.3",
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"diff-dom": "^5.1.4",
|
"diff-dom": "^5.1.4",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
|
"element-plus": "^2.9.6",
|
||||||
"emojis-keywords": "2.0.0",
|
"emojis-keywords": "2.0.0",
|
||||||
"emojis-list": "2.1.0",
|
"emojis-list": "2.1.0",
|
||||||
"esbuild-loader": "^4.3.0",
|
"esbuild-loader": "^4.3.0",
|
||||||
@@ -115,6 +117,8 @@
|
|||||||
"timeago-react": "^3.0.7",
|
"timeago-react": "^3.0.7",
|
||||||
"timeago.js": "^4.0.2",
|
"timeago.js": "^4.0.2",
|
||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
|
"veaury": "^2.6.2",
|
||||||
|
"vue-i18n": "^11.1.2",
|
||||||
"web-streams-polyfill": "^4.1.0",
|
"web-streams-polyfill": "^4.1.0",
|
||||||
"webpack": "^5.98.0",
|
"webpack": "^5.98.0",
|
||||||
"webpack-bundle-analyzer": "^4.10.2",
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
|
|||||||
@@ -1,29 +1,159 @@
|
|||||||
|
import 'element-plus/dist/index.css';
|
||||||
|
import '@undefined-moe/schemastery-vue/lib/schemastery-vue.css';
|
||||||
|
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
import Schema from 'schemastery';
|
import Schema from 'schemastery';
|
||||||
|
import { applyPureReactInVue, applyVueInReact, createReactMissVue } from 'veaury';
|
||||||
import Notification from 'vj/components/notification';
|
import Notification from 'vj/components/notification';
|
||||||
import { NamedPage } from 'vj/misc/Page';
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
import { request } from 'vj/utils';
|
import { i18n, request } from 'vj/utils';
|
||||||
|
|
||||||
|
const locales = {
|
||||||
|
zh: 'zh-CN',
|
||||||
|
en: 'en-US',
|
||||||
|
};
|
||||||
|
|
||||||
const page = new NamedPage('manage_config', async () => {
|
const page = new NamedPage('manage_config', async () => {
|
||||||
const schema = new Schema(UiContext.schema);
|
const [{ default: form }, { createI18n }, ElementPlus, { load }] = await Promise.all([
|
||||||
setInterval(() => {
|
import('@undefined-moe/schemastery-vue/lib/schemastery-vue.esm') as Promise<typeof import('@undefined-moe/schemastery-vue/src/index.ts')>,
|
||||||
try {
|
import('vue-i18n'),
|
||||||
const v = yaml.load($('#config').val().toString());
|
import('element-plus'),
|
||||||
schema(v);
|
import('vj/components/monaco/loader'),
|
||||||
$('#info').text('');
|
]);
|
||||||
} catch (e) {
|
const { monaco, registerAction, renderMarkdown } = await load(['yaml']);
|
||||||
console.debug(e);
|
const KForm = applyVueInReact(form.Form) as any;
|
||||||
$('#info').text(e.message);
|
|
||||||
}
|
function MarkdownContainer(props) {
|
||||||
}, 1000);
|
const rendered = React.useMemo(() => {
|
||||||
$('#submit').on('click', () => {
|
const res = renderMarkdown({ value: props.source });
|
||||||
const value = $('#config').val();
|
const value = res.element.innerHTML;
|
||||||
request.post('', { value }).then(() => {
|
res.dispose();
|
||||||
Notification.success('保存成功');
|
return value;
|
||||||
}).catch((e) => {
|
}, [props.source]);
|
||||||
Notification.error('保存失败:', e.message);
|
return <div dangerouslySetInnerHTML={{ __html: rendered }} />;
|
||||||
});
|
}
|
||||||
|
const [, ReactMissVue] = createReactMissVue({
|
||||||
|
useVueInjection() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
beforeVueAppMount(app: any) {
|
||||||
|
app.use(form);
|
||||||
|
app.use(createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: locales[i18n('__id')] || 'en-US',
|
||||||
|
}));
|
||||||
|
app.use(ElementPlus);
|
||||||
|
app.component('k-markdown', applyPureReactInVue(MarkdownContainer));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
const initialConfigString = UiContext.config;
|
||||||
|
const schema = new Schema(UiContext.schema);
|
||||||
|
const initial = yaml.load(UiContext.config);
|
||||||
|
|
||||||
|
function MonacoContainer({ config, setValue, setError }) {
|
||||||
|
const editorRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [editor, setEditor] = React.useState<any>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const listener = () => {
|
||||||
|
if (!editor) return;
|
||||||
|
editor.layout();
|
||||||
|
};
|
||||||
|
document.addEventListener('resize', listener);
|
||||||
|
return () => document.removeEventListener('resize', listener);
|
||||||
|
}, [editor]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!editorRef.current || loading) return;
|
||||||
|
setLoading(true);
|
||||||
|
const model = monaco.editor.createModel(config, 'yaml', monaco.Uri.parse('hydro://system/setting.yaml'));
|
||||||
|
const e = monaco.editor.create(editorRef.current, {
|
||||||
|
theme: 'vs-light',
|
||||||
|
lineNumbers: 'off',
|
||||||
|
glyphMargin: true,
|
||||||
|
lightbulb: { enabled: monaco.editor.ShowLightbulbIconMode.On },
|
||||||
|
model,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
hideCursorInOverviewRuler: true,
|
||||||
|
overviewRulerLanes: 0,
|
||||||
|
overviewRulerBorder: false,
|
||||||
|
fontFamily: UserContext.codeFontFamily,
|
||||||
|
fontLigatures: '',
|
||||||
|
unicodeHighlight: {
|
||||||
|
ambiguousCharacters: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
registerAction(e, model, editorRef.current);
|
||||||
|
e.onDidChangeModelContent(() => {
|
||||||
|
const val = e.getValue({ lineEnding: '\n', preserveBOM: false });
|
||||||
|
try {
|
||||||
|
const loaded = yaml.load(val);
|
||||||
|
schema(loaded);
|
||||||
|
setValue(val);
|
||||||
|
setError('');
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setEditor(e);
|
||||||
|
}, [editorRef.current]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!editor) return;
|
||||||
|
const current = editor.getValue({ lineEnding: '\n', preserveBOM: false });
|
||||||
|
if (current !== config) editor.setValue(config);
|
||||||
|
}, [editor, config]);
|
||||||
|
return (
|
||||||
|
<div ref={editorRef} style={{
|
||||||
|
width: '100%', height: '500px',
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialClone = JSON.parse(JSON.stringify(initial));
|
||||||
|
function App() {
|
||||||
|
const [value, setValue] = React.useState(initialClone);
|
||||||
|
const [info, setInfo] = React.useState('');
|
||||||
|
const [stringConfig, setStringConfig] = React.useState(initialConfigString);
|
||||||
|
|
||||||
|
const updateFromForm = React.useCallback((v) => {
|
||||||
|
const newDump = yaml.dump(v);
|
||||||
|
if (newDump === stringConfig) return;
|
||||||
|
console.log('update.form', newDump);
|
||||||
|
setStringConfig(newDump);
|
||||||
|
setValue(v);
|
||||||
|
}, [stringConfig]);
|
||||||
|
const updateFromMonaco = React.useCallback((v) => {
|
||||||
|
if (v === stringConfig) return;
|
||||||
|
console.log('update.monaco', v);
|
||||||
|
setStringConfig(v);
|
||||||
|
setValue(yaml.load(v));
|
||||||
|
}, [stringConfig]);
|
||||||
|
|
||||||
|
return <ReactMissVue>
|
||||||
|
<div className="row">
|
||||||
|
<KForm className="medium-7 columns" schema={schema} initial={initial} v-model={[value, updateFromForm]} />
|
||||||
|
<div className="medium-4 columns">
|
||||||
|
<MonacoContainer config={stringConfig} setValue={updateFromMonaco} setError={setInfo} />
|
||||||
|
<pre className="help-text">{info}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="row" style={{ marginTop: '20px' }}>
|
||||||
|
<div className="medium-10 columns">
|
||||||
|
<button onClick={() => {
|
||||||
|
request.post('', { value: stringConfig }).then(() => {
|
||||||
|
Notification.success('保存成功');
|
||||||
|
}).catch((e) => {
|
||||||
|
Notification.error('保存失败:', e.message);
|
||||||
|
});
|
||||||
|
}} className="rounded primary button">{i18n('Save All Changes')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ReactMissVue>;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('app')!).render(<App />);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default page;
|
export default page;
|
||||||
@@ -1,24 +1,10 @@
|
|||||||
{% extends "manage_base.html" %}
|
{% extends "manage_base.html" %}
|
||||||
{% block manage_content %}
|
{% block manage_content %}
|
||||||
|
</div>
|
||||||
|
<div class="medium-12 columns">
|
||||||
{{ set(UiContext, 'schema', schema) }}
|
{{ set(UiContext, 'schema', schema) }}
|
||||||
|
{{ set(UiContext, 'config', value) }}
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section__body typo">
|
<div class="section__body" id="app"></div>
|
||||||
<div class="row">
|
|
||||||
<div class="medium-10 columns form__item">
|
|
||||||
<label>
|
|
||||||
config
|
|
||||||
<div name="form_item_config" class="textarea-container">
|
|
||||||
<textarea id="config" data-yaml data-model="hydro://system/setting.yaml" placeholder="" class="textbox">{{ value }}</textarea>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<pre class="help-text" id="info"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="medium-10 columns form__item end">
|
|
||||||
<button id="submit" class="rounded primary button">{{ _('Save All Changes') }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in new issue
Block a user