mirror of
https://github.com/crawlab-team/crawlab.git
synced 2026-01-21 17:21:09 +01:00
updated plugin framework: allow adding routes and tabs
This commit is contained in:
@@ -1,3 +1,21 @@
|
||||
<template>
|
||||
<router-view/>
|
||||
<router-view />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {defineComponent, onBeforeMount} from 'vue';
|
||||
import {initPlugins} from '@/utils/plugin';
|
||||
import {useStore} from 'vuex';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
setup() {
|
||||
onBeforeMount(() => {
|
||||
const store = useStore();
|
||||
|
||||
initPlugins(store);
|
||||
});
|
||||
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
44
frontend/src/components/plugin/CreateEditPluginDialog.vue
Normal file
44
frontend/src/components/plugin/CreateEditPluginDialog.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<CreateEditDialog
|
||||
:action-functions="actionFunctions"
|
||||
:confirm-disabled="confirmDisabled"
|
||||
:confirm-loading="confirmLoading"
|
||||
:tab-name="createEditDialogTabName"
|
||||
:type="activeDialogKey"
|
||||
:visible="createEditDialogVisible"
|
||||
:form-rules="formRules"
|
||||
no-batch
|
||||
>
|
||||
<template #default>
|
||||
<PluginForm/>
|
||||
</template>
|
||||
</CreateEditDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import {useStore} from 'vuex';
|
||||
import CreateEditDialog from '@/components/dialog/CreateEditDialog.vue';
|
||||
import PluginForm from '@/components/plugin/PluginForm.vue';
|
||||
import usePlugin from '@/components/plugin/plugin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CreateEditProjectDialog',
|
||||
components: {
|
||||
CreateEditDialog,
|
||||
PluginForm,
|
||||
},
|
||||
setup() {
|
||||
// store
|
||||
const store = useStore();
|
||||
|
||||
return {
|
||||
...usePlugin(store),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
68
frontend/src/components/plugin/PluginForm.vue
Normal file
68
frontend/src/components/plugin/PluginForm.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<Form
|
||||
v-if="form"
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:selective="isSelectiveForm"
|
||||
>
|
||||
<!--Row-->
|
||||
<FormItem :span="2" :offset="2" label="Name" not-editable prop="name" required>
|
||||
<el-input v-model="form.name" disabled placeholder="Name" />
|
||||
</FormItem>
|
||||
<!--./Row-->
|
||||
|
||||
<!--Row-->
|
||||
<FormItem :span="2" label="Execute Command" prop="cmd">
|
||||
<el-input
|
||||
v-model="form.cmd"
|
||||
disabled
|
||||
placeholder="cmd"
|
||||
/>
|
||||
</FormItem>
|
||||
<!--./Row-->
|
||||
|
||||
<!--Row-->
|
||||
<FormItem :span="4" label="Description" prop="description">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
disabled
|
||||
placeholder="Description"
|
||||
type="textarea"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<!--./Row-->
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import {useStore} from 'vuex';
|
||||
import usePlugin from '@/components/plugin/plugin';
|
||||
import Form from '@/components/form/Form.vue';
|
||||
import FormItem from '@/components/form/FormItem.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PluginForm',
|
||||
props: {
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Form,
|
||||
FormItem,
|
||||
},
|
||||
setup(props, {emit}) {
|
||||
// store
|
||||
const store = useStore();
|
||||
|
||||
return {
|
||||
...usePlugin(store),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
30
frontend/src/components/plugin/plugin.ts
Normal file
30
frontend/src/components/plugin/plugin.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {readonly} from 'vue';
|
||||
import {Store} from 'vuex';
|
||||
import useForm from '@/components/form/form';
|
||||
import usePluginService from '@/services/plugin/pluginService';
|
||||
import {getDefaultFormComponentData} from '@/utils/form';
|
||||
|
||||
type Plugin = CPlugin;
|
||||
|
||||
// get new plugin
|
||||
export const getNewPlugin = (): Plugin => {
|
||||
return {};
|
||||
};
|
||||
|
||||
// form component data
|
||||
const formComponentData = getDefaultFormComponentData<Plugin>(getNewPlugin);
|
||||
|
||||
const usePlugin = (store: Store<RootStoreState>) => {
|
||||
// store
|
||||
const ns = 'plugin';
|
||||
|
||||
// form rules
|
||||
const formRules = readonly<FormRules>({});
|
||||
|
||||
return {
|
||||
...useForm(ns, store, usePluginService(store), formComponentData),
|
||||
formRules,
|
||||
};
|
||||
};
|
||||
|
||||
export default usePlugin;
|
||||
2
frontend/src/constants/plugin.ts
Normal file
2
frontend/src/constants/plugin.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const PLUGIN_UI_COMPONENT_TYPE_VIEW = 'view';
|
||||
export const PLUGIN_UI_COMPONENT_TYPE_TAB = 'tab';
|
||||
20
frontend/src/interfaces/models/plugin.d.ts
vendored
Normal file
20
frontend/src/interfaces/models/plugin.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
interface CPlugin extends BaseModel {
|
||||
name?: string;
|
||||
description?: string;
|
||||
type?: string;
|
||||
proto?: string;
|
||||
active?: boolean;
|
||||
endpoint?: string;
|
||||
cmd?: string;
|
||||
ui_components?: PluginUIComponent[];
|
||||
ui_sidebar_navs?: MenuItem[];
|
||||
}
|
||||
|
||||
interface PluginUIComponent {
|
||||
name?: string;
|
||||
title?: string;
|
||||
src?: string;
|
||||
type?: string;
|
||||
path?: string;
|
||||
parent_paths?: string[];
|
||||
}
|
||||
@@ -2,6 +2,7 @@ interface ListRequestParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
conditions?: FilterConditionData[] | string;
|
||||
all?: boolean | string | number;
|
||||
}
|
||||
|
||||
interface BatchRequestPayload {
|
||||
|
||||
8
frontend/src/interfaces/store/index.d.ts
vendored
8
frontend/src/interfaces/store/index.d.ts
vendored
@@ -14,6 +14,7 @@ declare global {
|
||||
schedule: ScheduleStoreState;
|
||||
user: UserStoreState;
|
||||
token: TokenStoreState;
|
||||
plugin: PluginStoreState;
|
||||
}
|
||||
|
||||
type StoreGetter<S, T> = (state: S, getters: StoreGetter<S, T>, rootState: RootStoreState, rootGetters: any) => T;
|
||||
@@ -103,6 +104,7 @@ declare global {
|
||||
collapseSidebar: StoreMutation<BaseStoreState<T>>;
|
||||
expandActions: StoreMutation<BaseStoreState<T>>;
|
||||
collapseActions: StoreMutation<BaseStoreState<T>>;
|
||||
setTabs: StoreMutation<BaseStoreState, NavItem[]>;
|
||||
setAfterSave: StoreMutation<BaseStoreState<T>, (() => Promise)[]>;
|
||||
}
|
||||
|
||||
@@ -133,7 +135,8 @@ declare global {
|
||||
| 'tag'
|
||||
| 'dataCollection'
|
||||
| 'user'
|
||||
| 'token';
|
||||
| 'token'
|
||||
| 'plugin';
|
||||
type ListStoreNamespace =
|
||||
'node'
|
||||
| 'project'
|
||||
@@ -143,7 +146,8 @@ declare global {
|
||||
| 'dataCollection'
|
||||
| 'schedule'
|
||||
| 'user'
|
||||
| 'token';
|
||||
| 'token'
|
||||
| 'plugin';
|
||||
|
||||
interface StoreContext<T> {
|
||||
namespace: StoreNamespace;
|
||||
|
||||
@@ -26,6 +26,7 @@ declare global {
|
||||
}
|
||||
|
||||
interface LayoutStoreMutations extends MutationTree<LayoutStoreState> {
|
||||
setMenuItems: StoreMutation<LayoutStoreState, MenuItem[]>;
|
||||
setSideBarCollapsed: StoreMutation<LayoutStoreState, boolean>;
|
||||
setTabs: StoreMutation<LayoutStoreState, Tab[]>;
|
||||
setActiveTabId: StoreMutation<LayoutStoreState, number>;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
type Node = CNode;
|
||||
|
||||
type NodeStoreModule = BaseModule<NodeStoreState, NodeStoreGetters, NodeStoreMutations, NodeStoreActions>;
|
||||
|
||||
type NodeStoreState = BaseStoreState<CNode>;
|
||||
|
||||
9
frontend/src/interfaces/store/modules/plugin.d.ts
vendored
Normal file
9
frontend/src/interfaces/store/modules/plugin.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
type PluginStoreModule = BaseModule<PluginStoreState, PluginStoreGetters, PluginStoreMutations, PluginStoreActions>;
|
||||
|
||||
type PluginStoreState = BaseStoreState<CPlugin>;
|
||||
|
||||
type PluginStoreGetters = BaseStoreGetters<CPlugin>;
|
||||
|
||||
type PluginStoreMutations = BaseStoreMutations<CPlugin>;
|
||||
|
||||
type PluginStoreActions = BaseStoreActions<CPlugin>;
|
||||
@@ -1,22 +1,23 @@
|
||||
<template>
|
||||
<el-container class="basic-layout">
|
||||
<Sidebar/>
|
||||
<Sidebar />
|
||||
<el-container :class="sidebarCollapsed ? 'collapsed' : ''" class="container">
|
||||
<Header/>
|
||||
<TabsView/>
|
||||
<Header />
|
||||
<TabsView />
|
||||
<div class="container-body">
|
||||
<router-view/>
|
||||
<router-view />
|
||||
</div>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent} from 'vue';
|
||||
import {computed, defineComponent, onMounted} from 'vue';
|
||||
import Header from './components/Header.vue';
|
||||
import Sidebar from './components/Sidebar.vue';
|
||||
import {useStore} from 'vuex';
|
||||
import TabsView from '@/layouts/components/TabsView.vue';
|
||||
import {initPlugins} from '@/utils/plugin';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicLayout',
|
||||
|
||||
@@ -63,7 +63,6 @@ export default defineComponent({
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const {layout} = store.state as RootStoreState;
|
||||
const {menuItems} = layout;
|
||||
const storeNamespace = 'layout';
|
||||
|
||||
const activePath = computed<string>(() => {
|
||||
@@ -72,6 +71,8 @@ export default defineComponent({
|
||||
|
||||
const sidebarCollapsed = computed<boolean>(() => layout.sidebarCollapsed);
|
||||
|
||||
const menuItems = computed<MenuItem[]>(() => layout.menuItems);
|
||||
|
||||
const toggleIcon = computed<string[]>(() => {
|
||||
if (sidebarCollapsed.value) {
|
||||
return ['fas', 'indent'];
|
||||
|
||||
@@ -6,6 +6,10 @@ import {plainClone} from '@/utils/object';
|
||||
import {getRoutePathByDepth} from '@/utils/route';
|
||||
import {ElMessage} from 'element-plus';
|
||||
|
||||
const IGNORE_GET_ALL_NS = [
|
||||
'task',
|
||||
];
|
||||
|
||||
const useDetail = <T = BaseModel>(ns: ListStoreNamespace) => {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -129,6 +133,7 @@ const useDetail = <T = BaseModel>(ns: ListStoreNamespace) => {
|
||||
|
||||
onBeforeMount(getForm);
|
||||
onBeforeMount(async () => {
|
||||
if (IGNORE_GET_ALL_NS.includes(ns)) return;
|
||||
await store.dispatch(`${ns}/getAllList`);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ const endpoint = '';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'Home',
|
||||
path: endpoint,
|
||||
component: () => import('@/views/home/Home.vue'),
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ import user from '@/router/user';
|
||||
import tag from '@/router/tag';
|
||||
import token from '@/router/token';
|
||||
import plugin from '@/router/plugin';
|
||||
import {initRouterAuth} from '@/router/auth';
|
||||
import {initRouterAuth} from '@/router/hooks/auth';
|
||||
import {sendPv} from '@/utils/admin';
|
||||
|
||||
export const routes: Array<RouteRecordRaw> = [
|
||||
|
||||
@@ -4,6 +4,7 @@ const endpoint = '/login';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'Login',
|
||||
path: endpoint,
|
||||
component: () => import('@/views/login/Login.vue'),
|
||||
},
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
import {defineAsyncComponent} from 'vue';
|
||||
import {RouteRecordRaw} from 'vue-router';
|
||||
import {getLoadModuleOptions, loadModule} from '@/utils/sfc';
|
||||
import {TAB_NAME_OVERVIEW} from '@/constants/tab';
|
||||
|
||||
const endpoint = 'plugins';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'PluginList',
|
||||
path: endpoint,
|
||||
// component: defineAsyncComponent(() => loadModule('/vue/HelloWorld.vue', getLoadModuleOptions())),
|
||||
component: defineAsyncComponent(() => loadModule('/vue/App.vue', getLoadModuleOptions())),
|
||||
component: () => import('@/views/plugin/list/PluginList.vue'),
|
||||
},
|
||||
{
|
||||
name: 'PluginDetail',
|
||||
path: `${endpoint}/:id`,
|
||||
redirect: to => {
|
||||
return {path: to.path + '/' + TAB_NAME_OVERVIEW};
|
||||
},
|
||||
component: () => import('@/views/plugin/detail/PluginDetail.vue'),
|
||||
children: [
|
||||
{
|
||||
path: TAB_NAME_OVERVIEW,
|
||||
component: () => import('@/views/plugin/detail/tabs/PluginDetailTabOverview.vue'),
|
||||
},
|
||||
]
|
||||
},
|
||||
] as Array<RouteRecordRaw>;
|
||||
|
||||
@@ -5,10 +5,12 @@ const endpoint = 'projects';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'ProjectList',
|
||||
path: endpoint,
|
||||
component: () => import('@/views/project/list/ProjectList.vue'),
|
||||
},
|
||||
{
|
||||
name: 'ProjectDetail',
|
||||
path: `${endpoint}/:id`,
|
||||
redirect: to => {
|
||||
return {path: to.path + '/overview'};
|
||||
|
||||
@@ -3,10 +3,12 @@ import {TAB_NAME_OVERVIEW, TAB_NAME_TASKS} from '@/constants/tab';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'ScheduleList',
|
||||
path: 'schedules',
|
||||
component: () => import('@/views/schedule/list/ScheduleList.vue'),
|
||||
},
|
||||
{
|
||||
name: 'ScheduleDetail',
|
||||
path: 'schedules/:id',
|
||||
redirect: to => {
|
||||
return {path: to.path + '/' + TAB_NAME_OVERVIEW};
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'SpiderList',
|
||||
path: 'spiders',
|
||||
component: () => import('@/views/spider/list/SpiderList.vue'),
|
||||
},
|
||||
{
|
||||
name: 'SpiderDetail',
|
||||
path: 'spiders/:id',
|
||||
redirect: to => {
|
||||
return {path: to.path + '/' + TAB_NAME_OVERVIEW};
|
||||
|
||||
@@ -5,10 +5,12 @@ const endpoint = 'tags';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'TagList',
|
||||
path: endpoint,
|
||||
component: () => import('@/views/tag/list/TagList.vue'),
|
||||
},
|
||||
{
|
||||
name: 'TagDetail',
|
||||
path: `${endpoint}/:id`,
|
||||
redirect: to => {
|
||||
return {path: to.path + '/overview'};
|
||||
|
||||
@@ -5,10 +5,12 @@ const endpoint = 'tasks';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'TaskList',
|
||||
path: endpoint,
|
||||
component: () => import('@/views/task/list/TaskList.vue'),
|
||||
},
|
||||
{
|
||||
name: 'TaskDetail',
|
||||
path: `${endpoint}/:id`,
|
||||
redirect: to => {
|
||||
return {path: to.path + '/overview'};
|
||||
|
||||
@@ -4,6 +4,7 @@ const endpoint = 'tokens';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'TokenList',
|
||||
path: endpoint,
|
||||
component: () => import('@/views/token/list/TokenList.vue'),
|
||||
},
|
||||
|
||||
@@ -3,10 +3,12 @@ import {TAB_NAME_OVERVIEW} from '@/constants/tab';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'UserList',
|
||||
path: 'users',
|
||||
component: () => import('@/views/user/list/UserList.vue'),
|
||||
},
|
||||
{
|
||||
name: 'UserDetail',
|
||||
path: 'users/:id',
|
||||
redirect: to => {
|
||||
return {path: to.path + '/' + TAB_NAME_OVERVIEW};
|
||||
|
||||
14
frontend/src/services/plugin/pluginService.ts
Normal file
14
frontend/src/services/plugin/pluginService.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {Store} from 'vuex';
|
||||
import {getDefaultService} from '@/utils/service';
|
||||
|
||||
type Plugin = CPlugin;
|
||||
|
||||
const usePluginService = (store: Store<RootStoreState>): Services<Plugin> => {
|
||||
const ns = 'plugin';
|
||||
|
||||
return {
|
||||
...getDefaultService<Plugin>(ns, store),
|
||||
};
|
||||
};
|
||||
|
||||
export default usePluginService;
|
||||
@@ -1,10 +1,11 @@
|
||||
import axios, {AxiosRequestConfig} from 'axios';
|
||||
import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
|
||||
import {ElMessageBox} from 'element-plus';
|
||||
import router from '@/router';
|
||||
import {getRequestBaseUrl} from '@/utils/request';
|
||||
|
||||
// TODO: request interception
|
||||
|
||||
// TODO: response interception
|
||||
// response interception
|
||||
let msgBoxVisible = false;
|
||||
axios.interceptors.response.use(res => {
|
||||
return res;
|
||||
@@ -24,13 +25,9 @@ axios.interceptors.response.use(res => {
|
||||
});
|
||||
|
||||
const useRequest = () => {
|
||||
// implementation
|
||||
const baseUrl = process.env.VUE_APP_API_BASE_URL || 'http://localhost:8000';
|
||||
|
||||
const request = async <R = any>(opts: AxiosRequestConfig): Promise<R> => {
|
||||
// base url
|
||||
const baseURL = baseUrl;
|
||||
const baseUrl = getRequestBaseUrl();
|
||||
|
||||
const getHeaders = (): any => {
|
||||
// headers
|
||||
const headers = {} as any;
|
||||
|
||||
@@ -40,6 +37,16 @@ const useRequest = () => {
|
||||
headers['Authorization'] = token;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
const request = async <R = any>(opts: AxiosRequestConfig): Promise<R> => {
|
||||
// base url
|
||||
const baseURL = baseUrl;
|
||||
|
||||
// headers
|
||||
const headers = getHeaders();
|
||||
|
||||
// axios response
|
||||
const res = await axios.request({
|
||||
...opts,
|
||||
@@ -112,7 +119,7 @@ const useRequest = () => {
|
||||
};
|
||||
|
||||
const getAll = async <T = any>(url: string, opts?: AxiosRequestConfig) => {
|
||||
return await getList(url, {}, opts);
|
||||
return await getList(url, {all: true}, opts);
|
||||
};
|
||||
|
||||
const postList = async <T = any, R = Response, PM = any>(url: string, data?: BatchRequestPayloadWithJsonStringData, params?: PM, opts?: AxiosRequestConfig): Promise<R> => {
|
||||
@@ -127,6 +134,31 @@ const useRequest = () => {
|
||||
return await del<BatchRequestPayload, R, PM>(url, data, params, opts);
|
||||
};
|
||||
|
||||
const requestRaw = async <R = any>(opts: AxiosRequestConfig): Promise<AxiosResponse> => {
|
||||
// base url
|
||||
const baseURL = baseUrl;
|
||||
|
||||
// headers
|
||||
const headers = getHeaders();
|
||||
|
||||
// axios response
|
||||
return await axios.request({
|
||||
...opts,
|
||||
baseURL,
|
||||
headers,
|
||||
});
|
||||
};
|
||||
|
||||
const getRaw = async <T = any, PM = any>(url: string, params?: PM, opts?: AxiosRequestConfig): Promise<AxiosResponse> => {
|
||||
opts = {
|
||||
...opts,
|
||||
method: 'GET',
|
||||
url,
|
||||
params,
|
||||
};
|
||||
return await requestRaw(opts);
|
||||
};
|
||||
|
||||
return {
|
||||
// public variables and methods
|
||||
baseUrl,
|
||||
@@ -140,6 +172,8 @@ const useRequest = () => {
|
||||
postList,
|
||||
putList,
|
||||
delList,
|
||||
requestRaw,
|
||||
getRaw,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import dataCollection from '@/store/modules/dataCollection';
|
||||
import schedule from '@/store/modules/schedule';
|
||||
import user from '@/store/modules/user';
|
||||
import token from '@/store/modules/token';
|
||||
import plugin from '@/store/modules/plugin';
|
||||
|
||||
export default createStore<RootStoreState>({
|
||||
modules: {
|
||||
@@ -26,5 +27,6 @@ export default createStore<RootStoreState>({
|
||||
schedule,
|
||||
user,
|
||||
token,
|
||||
plugin,
|
||||
},
|
||||
}) as Store<RootStoreState>;
|
||||
|
||||
@@ -35,6 +35,9 @@ export default {
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
setMenuItems(state: LayoutStoreState, items: MenuItem[]) {
|
||||
state.menuItems = items;
|
||||
},
|
||||
setSideBarCollapsed(state: LayoutStoreState, value: boolean) {
|
||||
state.sidebarCollapsed = value;
|
||||
},
|
||||
|
||||
32
frontend/src/store/modules/plugin.ts
Normal file
32
frontend/src/store/modules/plugin.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
getDefaultStoreActions,
|
||||
getDefaultStoreGetters,
|
||||
getDefaultStoreMutations,
|
||||
getDefaultStoreState
|
||||
} from '@/utils/store';
|
||||
|
||||
type Plugin = CPlugin;
|
||||
|
||||
const state = {
|
||||
...getDefaultStoreState<Plugin>('plugin'),
|
||||
} as PluginStoreState;
|
||||
|
||||
const getters = {
|
||||
...getDefaultStoreGetters<Plugin>(),
|
||||
} as PluginStoreGetters;
|
||||
|
||||
const mutations = {
|
||||
...getDefaultStoreMutations<Plugin>(),
|
||||
} as PluginStoreMutations;
|
||||
|
||||
const actions = {
|
||||
...getDefaultStoreActions<Plugin>('/plugins'),
|
||||
} as PluginStoreActions;
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions,
|
||||
} as PluginStoreModule;
|
||||
124
frontend/src/utils/plugin.ts
Normal file
124
frontend/src/utils/plugin.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import {Store} from 'vuex';
|
||||
import {cloneArray} from '@/utils/object';
|
||||
import router from '@/router';
|
||||
import {PLUGIN_UI_COMPONENT_TYPE_TAB, PLUGIN_UI_COMPONENT_TYPE_VIEW} from '@/constants/plugin';
|
||||
import {loadModule} from '@/utils/sfc';
|
||||
|
||||
type Plugin = CPlugin;
|
||||
|
||||
const PLUGIN_PROXY_ENDPOINT = '/plugin-proxy';
|
||||
|
||||
const getStoreNamespaceFromRoutePath = (path: string): ListStoreNamespace => {
|
||||
const arr = path.split('/');
|
||||
let ns = arr[1];
|
||||
if (ns.endsWith('s')) {
|
||||
ns = ns.substr(0, ns.length - 1);
|
||||
}
|
||||
return ns as ListStoreNamespace;
|
||||
};
|
||||
|
||||
const initPluginSidebarMenuItems = (store: Store<RootStoreState>) => {
|
||||
const {
|
||||
layout,
|
||||
plugin: state,
|
||||
} = store.state;
|
||||
|
||||
// sidebar menu items
|
||||
const menuItems = cloneArray(layout.menuItems);
|
||||
|
||||
// add plugin nav to sidebar navs
|
||||
state.allList.forEach(p => {
|
||||
p.ui_sidebar_navs?.forEach(nav => {
|
||||
const sidebarPaths = layout.menuItems.map(d => d.path);
|
||||
if (!sidebarPaths.includes(nav.path)) {
|
||||
menuItems.push(nav);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// set sidebar menu items
|
||||
store.commit(`layout/setMenuItems`, menuItems);
|
||||
};
|
||||
|
||||
const addPluginRouteTab = (store: Store<RootStoreState>, p: Plugin, pc: PluginUIComponent) => {
|
||||
const routesPaths = router.getRoutes().map(r => r.path);
|
||||
pc.parent_paths?.forEach(parentPath => {
|
||||
// plugin route path
|
||||
const pluginPath = `${parentPath}/${pc.path}`;
|
||||
|
||||
// skip if new route path is already in the routes
|
||||
if (routesPaths.includes(pluginPath)) return;
|
||||
|
||||
// parent route
|
||||
const parentRoute = router.getRoutes().find(r => r.path === parentPath);
|
||||
if (!parentRoute) return;
|
||||
|
||||
// add route
|
||||
router.addRoute(parentRoute.name?.toString() as string, {
|
||||
name: `${parentRoute.name?.toString()}-${pc.name}`,
|
||||
path: pc.path as string,
|
||||
component: () => loadModule(`${PLUGIN_PROXY_ENDPOINT}/${p.name}/${pc.src}`)
|
||||
});
|
||||
|
||||
// add tab
|
||||
const ns = getStoreNamespaceFromRoutePath(pluginPath);
|
||||
const state = store.state[ns];
|
||||
const tabs = cloneArray(state.tabs);
|
||||
if (tabs.map(t => t.id).includes(pc.name as string)) return;
|
||||
tabs.push({
|
||||
id: pc.name as string,
|
||||
title: pc.title,
|
||||
});
|
||||
store.commit(`${ns}/setTabs`, tabs);
|
||||
});
|
||||
};
|
||||
|
||||
const addPluginRouteView = (store: Store<RootStoreState>, pc: PluginUIComponent) => {
|
||||
// TODO: implement
|
||||
};
|
||||
|
||||
const initPluginRoutes = (store: Store<RootStoreState>) => {
|
||||
// store
|
||||
const {
|
||||
plugin: state,
|
||||
} = store.state as RootStoreState;
|
||||
|
||||
// add plugin routes
|
||||
state.allList.forEach(p => {
|
||||
p.ui_components?.forEach(pc => {
|
||||
// skip if path is empty
|
||||
if (!pc.path) return;
|
||||
|
||||
switch (pc.type) {
|
||||
case PLUGIN_UI_COMPONENT_TYPE_VIEW:
|
||||
addPluginRouteView(store, pc);
|
||||
break;
|
||||
case PLUGIN_UI_COMPONENT_TYPE_TAB:
|
||||
addPluginRouteTab(store, p, pc);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const initPlugins = async (store: Store<RootStoreState>) => {
|
||||
// store
|
||||
const ns = 'plugin';
|
||||
const {
|
||||
plugin: state,
|
||||
} = store.state as RootStoreState;
|
||||
|
||||
// skip if not logged-in
|
||||
if (!localStorage.getItem('token')) return;
|
||||
|
||||
// skip if all plugin list is already fetched
|
||||
if (state.allList.length) return;
|
||||
|
||||
// get all plugin list
|
||||
await store.dispatch(`${ns}/getAllList`);
|
||||
|
||||
initPluginSidebarMenuItems(store);
|
||||
|
||||
initPluginRoutes(store);
|
||||
};
|
||||
|
||||
3
frontend/src/utils/request.ts
Normal file
3
frontend/src/utils/request.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const getRequestBaseUrl = (): string => {
|
||||
return process.env.VUE_APP_API_BASE_URL || 'http://localhost:8000';
|
||||
};
|
||||
@@ -1,7 +1,14 @@
|
||||
import * as vue from '@vue/runtime-dom';
|
||||
import {getRequestBaseUrl} from '@/utils/request';
|
||||
import useRequest from '@/services/request';
|
||||
|
||||
const {loadModule: sfcLoadModule} = window['vue3-sfc-loader'];
|
||||
|
||||
export const getLoadModuleOptions = (): any => {
|
||||
const {
|
||||
getRaw,
|
||||
} = useRequest();
|
||||
|
||||
const getLoadModuleOptions = (): any => {
|
||||
return {
|
||||
moduleCache: {
|
||||
vue,
|
||||
@@ -20,12 +27,9 @@ export const getLoadModuleOptions = (): any => {
|
||||
return String(new URL(relPath.toString(), refPath === undefined ? window.location.toString() : refPath.toString()));
|
||||
},
|
||||
async getFile(url: string) {
|
||||
const res = await fetch(url.toString());
|
||||
if (!res.ok) {
|
||||
throw Object.assign(new Error(res.statusText + ' ' + url), {res});
|
||||
}
|
||||
const res = await getRaw(url.toString());
|
||||
return {
|
||||
getContentData: (asBinary: boolean) => asBinary ? res.arrayBuffer() : res.text(),
|
||||
getContentData: async (_: boolean) => res.data,
|
||||
};
|
||||
},
|
||||
addStyle(textContent: string) {
|
||||
@@ -36,4 +40,4 @@ export const getLoadModuleOptions = (): any => {
|
||||
};
|
||||
};
|
||||
|
||||
export const loadModule = sfcLoadModule;
|
||||
export const loadModule = (path: string) => sfcLoadModule(`${getRequestBaseUrl()}${path}`, getLoadModuleOptions());
|
||||
|
||||
@@ -188,6 +188,9 @@ export const getDefaultStoreMutations = <T = any>(): BaseStoreMutations<T> => {
|
||||
collapseActions: (state: BaseStoreState<T>) => {
|
||||
state.actionsCollapsed = true;
|
||||
},
|
||||
setTabs: (state: BaseStoreState<T>, tabs) => {
|
||||
state.tabs = tabs;
|
||||
},
|
||||
setAfterSave: (state: BaseStoreState<T>, fnList) => {
|
||||
state.afterSave = fnList;
|
||||
},
|
||||
|
||||
21
frontend/src/views/plugin/detail/PluginDetail.vue
Normal file
21
frontend/src/views/plugin/detail/PluginDetail.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<DetailLayout store-namespace="plugin">
|
||||
</DetailLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import DetailLayout from '@/layouts/DetailLayout.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PluginDetail',
|
||||
components: {DetailLayout},
|
||||
setup(props, {emit}) {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="plugin-detail-tab-overview">
|
||||
<PluginForm/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import PluginForm from '@/components/plugin/PluginForm.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PluginDetailTabOverview',
|
||||
components: {
|
||||
PluginForm,
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.plugin-detail-tab-overview {
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
41
frontend/src/views/plugin/list/PluginList.vue
Normal file
41
frontend/src/views/plugin/list/PluginList.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<ListLayout
|
||||
:action-functions="actionFunctions"
|
||||
:nav-actions="navActions"
|
||||
:pagination="tablePagination"
|
||||
:table-columns="tableColumns"
|
||||
:table-data="tableData"
|
||||
:table-total="tableTotal"
|
||||
class="plugin-list"
|
||||
>
|
||||
<template #extra>
|
||||
<!-- Dialogs (handled by store) -->
|
||||
<CreateEditPluginDialog/>
|
||||
<!-- ./Dialogs -->
|
||||
</template>
|
||||
</ListLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue';
|
||||
import ListLayout from '@/layouts/ListLayout.vue';
|
||||
import usePluginList from '@/views/plugin/list/pluginList';
|
||||
import CreateEditPluginDialog from '@/components/plugin/CreateEditPluginDialog.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PluginList',
|
||||
components: {
|
||||
ListLayout,
|
||||
CreateEditPluginDialog,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
...usePluginList(),
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
123
frontend/src/views/plugin/list/pluginList.ts
Normal file
123
frontend/src/views/plugin/list/pluginList.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import useList from '@/layouts/list';
|
||||
import {useStore} from 'vuex';
|
||||
import {getDefaultUseListOptions, setupListComponent} from '@/utils/list';
|
||||
import {computed, h} from 'vue';
|
||||
import {TABLE_COLUMN_NAME_ACTIONS} from '@/constants/table';
|
||||
import {ElMessageBox} from 'element-plus';
|
||||
import usePluginService from '@/services/plugin/pluginService';
|
||||
import NavLink from '@/components/nav/NavLink.vue';
|
||||
import {useRouter} from 'vue-router';
|
||||
|
||||
type Plugin = CPlugin;
|
||||
|
||||
const usePluginList = () => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
|
||||
// store
|
||||
const ns = 'plugin';
|
||||
const store = useStore<RootStoreState>();
|
||||
const {commit} = store;
|
||||
|
||||
// services
|
||||
const {
|
||||
getList,
|
||||
deleteById,
|
||||
} = usePluginService(store);
|
||||
|
||||
// nav actions
|
||||
const navActions = computed<ListActionGroup[]>(() => [
|
||||
{
|
||||
name: 'common',
|
||||
children: [
|
||||
{
|
||||
buttonType: 'label',
|
||||
label: 'New Plugin',
|
||||
tooltip: 'New Plugin',
|
||||
icon: ['fa', 'plus'],
|
||||
type: 'success',
|
||||
onClick: () => {
|
||||
commit(`${ns}/showDialog`, 'create');
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
// table columns
|
||||
const tableColumns = computed<TableColumns<Plugin>>(() => [
|
||||
{
|
||||
key: 'name', // name
|
||||
label: 'Name',
|
||||
icon: ['fa', 'font'],
|
||||
width: '150',
|
||||
value: (row: Plugin) => h(NavLink, {
|
||||
path: `/plugins/${row._id}`,
|
||||
label: row.name,
|
||||
}),
|
||||
hasSort: true,
|
||||
hasFilter: true,
|
||||
allowFilterSearch: true,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: 'Description',
|
||||
icon: ['fa', 'comment-alt'],
|
||||
width: 'auto',
|
||||
hasFilter: true,
|
||||
allowFilterSearch: true,
|
||||
},
|
||||
{
|
||||
key: TABLE_COLUMN_NAME_ACTIONS,
|
||||
label: 'Actions',
|
||||
fixed: 'right',
|
||||
width: '200',
|
||||
buttons: [
|
||||
{
|
||||
type: 'primary',
|
||||
icon: ['fa', 'search'],
|
||||
tooltip: 'View',
|
||||
onClick: (row) => {
|
||||
router.push(`/plugins/${row._id}`);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// type: 'info',
|
||||
// size: 'mini',
|
||||
// icon: ['fa', 'clone'],
|
||||
// tooltip: 'Clone',
|
||||
// onClick: (row) => {
|
||||
// console.log('clone', row);
|
||||
// }
|
||||
// },
|
||||
{
|
||||
type: 'danger',
|
||||
size: 'mini',
|
||||
icon: ['fa', 'trash-alt'],
|
||||
tooltip: 'Delete',
|
||||
disabled: (row: Plugin) => !!row.active,
|
||||
onClick: async (row: Plugin) => {
|
||||
const res = await ElMessageBox.confirm('Are you sure to delete?', 'Delete');
|
||||
if (res) {
|
||||
await deleteById(row._id as string);
|
||||
}
|
||||
await getList();
|
||||
},
|
||||
},
|
||||
],
|
||||
disableTransfer: true,
|
||||
}
|
||||
]);
|
||||
|
||||
// options
|
||||
const opts = getDefaultUseListOptions<Plugin>(navActions, tableColumns);
|
||||
|
||||
// init
|
||||
setupListComponent(ns, store, []);
|
||||
|
||||
return {
|
||||
...useList<Plugin>(ns, store, opts)
|
||||
};
|
||||
};
|
||||
|
||||
export default usePluginList;
|
||||
Reference in New Issue
Block a user