mirror of
https://github.com/directus/directus.git
synced 2026-02-01 23:45:02 -05:00
Add panels as extension type
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { getDisplays } from '@/displays';
|
||||
import { getInterfaces } from '@/interfaces';
|
||||
import { getPanels } from '@/panels';
|
||||
import { getLayouts } from '@/layouts';
|
||||
import { getModules } from '@/modules';
|
||||
import { useCollectionsStore, useFieldsStore } from '@/stores';
|
||||
@@ -10,6 +11,7 @@ import { i18n, Language, loadedLanguages } from './index';
|
||||
const { modules, modulesRaw } = getModules();
|
||||
const { layouts, layoutsRaw } = getLayouts();
|
||||
const { interfaces, interfacesRaw } = getInterfaces();
|
||||
const { panels, panelsRaw } = getPanels();
|
||||
const { displays, displaysRaw } = getDisplays();
|
||||
|
||||
export async function setLanguage(lang: Language): Promise<boolean> {
|
||||
@@ -33,6 +35,7 @@ export async function setLanguage(lang: Language): Promise<boolean> {
|
||||
modules.value = translate(modulesRaw.value);
|
||||
layouts.value = translate(layoutsRaw.value);
|
||||
interfaces.value = translate(interfacesRaw.value);
|
||||
panels.value = translate(panelsRaw.value);
|
||||
displays.value = translate(displaysRaw.value);
|
||||
|
||||
collectionsStore.translateCollections();
|
||||
|
||||
@@ -1176,3 +1176,7 @@ layouts:
|
||||
calendar: Calendar
|
||||
start_date_field: Start Date Field
|
||||
end_date_field: End Date Field
|
||||
panels:
|
||||
metric:
|
||||
name: Metric
|
||||
description: Show a single value based on a query
|
||||
|
||||
@@ -50,13 +50,14 @@ import './views/register';
|
||||
|
||||
import { registerInterfaces } from './interfaces/register';
|
||||
import { loadModules } from './modules/register';
|
||||
import { registerPanels } from './panels/register';
|
||||
import { registerLayouts } from './layouts/register';
|
||||
import { registerDisplays } from './displays/register';
|
||||
|
||||
import App from './app.vue';
|
||||
|
||||
async function init() {
|
||||
await Promise.all([registerInterfaces(), registerDisplays(), registerLayouts(), loadModules()]);
|
||||
await Promise.all([registerInterfaces(), registerPanels(), registerDisplays(), registerLayouts(), loadModules()]);
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineModule } from '@/modules/define';
|
||||
import InsightsOverview from './routes/overview.vue';
|
||||
import InsightsDashboard from './routes/dashboard.vue';
|
||||
import InsightsPanelConfiguration from './routes/panel-configuration.vue';
|
||||
|
||||
export default defineModule({
|
||||
id: 'insights',
|
||||
@@ -17,6 +18,16 @@ export default defineModule({
|
||||
path: '/:primaryKey',
|
||||
component: InsightsDashboard,
|
||||
props: true,
|
||||
children: [
|
||||
{
|
||||
name: 'panel-detail',
|
||||
path: ':panelKey',
|
||||
props: true,
|
||||
components: {
|
||||
detail: InsightsPanelConfiguration,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
order: 30,
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<v-button v-if="editMode" rounded icon outlined :to="`/insights/${currentDashboard.id}/panel/+`">
|
||||
<v-icon name="add" />
|
||||
</v-button>
|
||||
|
||||
<v-button rounded icon @click="editMode = !editMode">
|
||||
<v-icon :name="editMode ? 'check' : 'edit'" />
|
||||
</v-button>
|
||||
@@ -17,7 +21,11 @@
|
||||
<insights-navigation />
|
||||
</template>
|
||||
|
||||
<div class="workspace" :class="{ editing: editMode }"></div>
|
||||
<div class="workspace" :class="{ editing: editMode }">
|
||||
<!-- <div class="dummy" /> -->
|
||||
</div>
|
||||
|
||||
<router-view name="detail" :dashboard-key="primaryKey" />
|
||||
</private-view>
|
||||
</template>
|
||||
|
||||
@@ -44,7 +52,13 @@ export default defineComponent({
|
||||
insightsStore.state.dashboards.find((dashboard) => dashboard.id === props.primaryKey)
|
||||
);
|
||||
|
||||
return { currentDashboard, editMode };
|
||||
const stagedPanels = ref([]);
|
||||
|
||||
const panels = computed(() => {
|
||||
return currentDashboard.value?.panels || [];
|
||||
});
|
||||
|
||||
return { currentDashboard, editMode, panels };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -61,6 +75,10 @@ export default defineComponent({
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.workspace > * {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.workspace::before {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
@@ -68,7 +86,7 @@ export default defineComponent({
|
||||
display: block;
|
||||
width: calc(100% + 8px);
|
||||
height: calc(100% + 8px);
|
||||
background-image: radial-gradient(#efefef 25%, transparent 25%);
|
||||
background-image: radial-gradient(#efefef 20%, transparent 20%);
|
||||
background-position: -6px -6px;
|
||||
background-size: 20px 20px;
|
||||
opacity: 0;
|
||||
@@ -80,4 +98,17 @@ export default defineComponent({
|
||||
.workspace.editing::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* .dummy {
|
||||
--pos-x: 5;
|
||||
--pos-y: 5;
|
||||
--width: 5;
|
||||
--height: 5;
|
||||
|
||||
display: block;
|
||||
grid-row: var(--pos-y) / span var(--height);
|
||||
grid-column: var(--pos-x) / span var(--width);
|
||||
background-color: var(--primary);
|
||||
border-radius: var(--border-radius);
|
||||
} */
|
||||
</style>
|
||||
|
||||
80
app/src/modules/insights/routes/panel-configuration.vue
Normal file
80
app/src/modules/insights/routes/panel-configuration.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<v-drawer active :title="values.name || $t('panel')">
|
||||
<div class="content">
|
||||
<v-fancy-select class="select" :items="selectItems" v-model="values.type" />
|
||||
</div>
|
||||
</v-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from '@vue/composition-api';
|
||||
import { useInsightsStore } from '@/stores';
|
||||
import { getPanels } from '@/panels';
|
||||
import { FancySelectItem } from '@/components/v-fancy-select/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PanelConfiguration',
|
||||
props: {
|
||||
primaryKey: {
|
||||
type: String,
|
||||
default: '+',
|
||||
},
|
||||
dashboardKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { panels } = getPanels();
|
||||
|
||||
const insightsStore = useInsightsStore();
|
||||
|
||||
const existing = computed(() =>
|
||||
insightsStore.state.dashboards
|
||||
.find((dashboard) => dashboard.id === props.dashboardKey)!
|
||||
.panels.find((panel) => panel.id === props.primaryKey)
|
||||
);
|
||||
|
||||
const edits = ref({});
|
||||
|
||||
const values = computed(() => {
|
||||
if (existing.value) return { ...existing.value, ...edits.value };
|
||||
return edits.value;
|
||||
});
|
||||
|
||||
const selectItems = computed<FancySelectItem[]>(() => {
|
||||
return panels.value.map((panel) => {
|
||||
const item: FancySelectItem = {
|
||||
text: panel.name,
|
||||
icon: panel.icon,
|
||||
description: panel.description,
|
||||
value: panel.id,
|
||||
};
|
||||
|
||||
return item;
|
||||
});
|
||||
});
|
||||
|
||||
return { existing, values, selectItems };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
padding: var(--content-padding);
|
||||
padding-top: 0;
|
||||
padding-bottom: var(--content-padding);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--
|
||||
|
||||
type
|
||||
options
|
||||
--
|
||||
toggle name
|
||||
icon color
|
||||
note
|
||||
|
||||
-->
|
||||
13
app/src/panels/define.ts
Normal file
13
app/src/panels/define.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { PanelConfig, PanelDefineParam } from './types';
|
||||
|
||||
export function definePanel(config: PanelDefineParam): PanelConfig {
|
||||
let options: PanelConfig;
|
||||
|
||||
if (typeof config === 'function') {
|
||||
options = config();
|
||||
} else {
|
||||
options = config;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
17
app/src/panels/index.ts
Normal file
17
app/src/panels/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ref, Ref } from '@vue/composition-api';
|
||||
import { PanelConfig } from './types';
|
||||
|
||||
let panelsRaw: Ref<PanelConfig[]>;
|
||||
let panels: Ref<PanelConfig[]>;
|
||||
|
||||
export function getPanels(): Record<string, Ref<PanelConfig[]>> {
|
||||
if (!panelsRaw) {
|
||||
panelsRaw = ref([]);
|
||||
}
|
||||
|
||||
if (!panels) {
|
||||
panels = ref([]);
|
||||
}
|
||||
|
||||
return { panels, panelsRaw };
|
||||
}
|
||||
13
app/src/panels/metric/index.ts
Normal file
13
app/src/panels/metric/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { definePanel } from '../define';
|
||||
import PanelMetric from './metric.vue';
|
||||
|
||||
export default definePanel({
|
||||
id: 'metric',
|
||||
name: '$t:panels.metric.name',
|
||||
description: '$t:panels.metric.description',
|
||||
icon: 'functions',
|
||||
component: PanelMetric,
|
||||
options: [],
|
||||
minWidth: 4,
|
||||
minHeight: 4,
|
||||
});
|
||||
3
app/src/panels/metric/metric.vue
Normal file
3
app/src/panels/metric/metric.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>Metric</div>
|
||||
</template>
|
||||
47
app/src/panels/register.ts
Normal file
47
app/src/panels/register.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// import api from '@/api';
|
||||
// import { getRootPath } from '@/utils/get-root-path';
|
||||
import registerComponent from '@/utils/register-component/';
|
||||
// import asyncPool from 'tiny-async-pool';
|
||||
import { Component } from 'vue';
|
||||
import { getPanels } from './index';
|
||||
import { PanelConfig } from './types';
|
||||
|
||||
const { panelsRaw } = getPanels();
|
||||
|
||||
export async function registerPanels(): Promise<void> {
|
||||
const context = require.context('.', true, /^.*index\.ts$/);
|
||||
|
||||
const modules = context
|
||||
.keys()
|
||||
.map((key) => context(key))
|
||||
.map((mod) => mod.default)
|
||||
.filter((m) => m);
|
||||
|
||||
// try {
|
||||
// const customResponse = await api.get('/extensions/panels/');
|
||||
// const panels: string[] = customResponse.data.data || [];
|
||||
|
||||
// await asyncPool(5, panels, async (panelName) => {
|
||||
// try {
|
||||
// const result = await import(
|
||||
// /* webpackIgnore: true */ getRootPath() + `extensions/panels/${panelName}/index.js`
|
||||
// );
|
||||
// modules.push(result.default);
|
||||
// } catch (err) {
|
||||
// console.warn(`Couldn't load custom panel "${panelName}":`, err);
|
||||
// }
|
||||
// });
|
||||
// } catch {
|
||||
// console.warn(`Couldn't load custom panels`);
|
||||
// }
|
||||
|
||||
panelsRaw.value = modules;
|
||||
|
||||
panelsRaw.value.forEach((panel: PanelConfig) => {
|
||||
registerComponent('panel-' + panel.id, panel.component);
|
||||
|
||||
if (typeof panel.options !== 'function' && Array.isArray(panel.options) === false) {
|
||||
registerComponent(`panel-options-${panel.id}`, panel.options as Component);
|
||||
}
|
||||
});
|
||||
}
|
||||
17
app/src/panels/types.ts
Normal file
17
app/src/panels/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Field } from '@/types';
|
||||
import { AsyncComponent, Component } from 'vue';
|
||||
import VueI18n from 'vue-i18n';
|
||||
|
||||
export interface PanelConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
description?: string | VueI18n.TranslateResult;
|
||||
component: Component | AsyncComponent;
|
||||
options: DeepPartial<Field>[] | Component | AsyncComponent;
|
||||
minWidth: number;
|
||||
minHeight: number;
|
||||
}
|
||||
|
||||
export type PanelDefineParam = PanelDefineParamGeneric<PanelConfig>;
|
||||
export type PanelDefineParamGeneric<T> = T | (() => T);
|
||||
Reference in New Issue
Block a user