mirror of
https://github.com/directus/directus.git
synced 2026-02-05 11:04:55 -05:00
Add "move" option, various tweaks
This commit is contained in:
@@ -122,6 +122,9 @@ field_update_success: 'Updated Field: "{field}"'
|
||||
duplicate_where_to: Where would you like to duplicate this field to?
|
||||
language: Language
|
||||
aggregate_function: Aggregate Function
|
||||
aggregate_precision: Aggregate Precision
|
||||
group_aggregation: Group Aggregation
|
||||
group_precision: Group Precision
|
||||
global: Global
|
||||
admins_have_all_permissions: Admins have all permissions
|
||||
camera: Camera
|
||||
@@ -376,10 +379,15 @@ collection_field_not_setup: The collection field option is misconfigured
|
||||
select_a_collection: Select a Collection
|
||||
select_a_field: Select a Field
|
||||
active: Active
|
||||
move_to: Move To...
|
||||
users: Users
|
||||
activity: Activity
|
||||
webhooks: Webhooks
|
||||
decimals: Decimals
|
||||
value_decimals: Value Decimals
|
||||
min_value: Min Value
|
||||
max_value: Max Value
|
||||
automatic: Automatic
|
||||
field_width: Field Width
|
||||
add_filter: Add Filter
|
||||
upper_limit: Upper limit...
|
||||
|
||||
@@ -21,13 +21,6 @@
|
||||
</div>
|
||||
|
||||
<div class="edit-actions" v-if="editMode" @pointerdown.stop>
|
||||
<v-icon
|
||||
class="duplicate-icon"
|
||||
name="control_point_duplicate"
|
||||
v-tooltip="t('duplicate')"
|
||||
@click.stop="$emit('duplicate')"
|
||||
clickable
|
||||
/>
|
||||
<v-icon
|
||||
class="edit-icon"
|
||||
name="edit"
|
||||
@@ -35,7 +28,37 @@
|
||||
@click.stop="$router.push(`/insights/${panel.dashboard}/${panel.id}`)"
|
||||
clickable
|
||||
/>
|
||||
<v-icon clickable class="delete-icon" name="clear" v-tooltip="t('delete')" @click.stop="$emit('delete')" />
|
||||
|
||||
<v-menu placement="bottom-end" show-arrow>
|
||||
<template #activator="{ toggle }">
|
||||
<v-icon class="more-icon" name="more_vert" @click="toggle" clickable />
|
||||
</template>
|
||||
|
||||
<v-list>
|
||||
<v-list-item @click="$emit('move')" clickable :disabled="panel.id.startsWith('_')">
|
||||
<v-list-item-icon>
|
||||
<v-icon class="move-icon" name="input" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
{{ t('move_to') }}
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="$emit('duplicate')" clickable>
|
||||
<v-list-item-icon>
|
||||
<v-icon name="control_point_duplicate" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>{{ t('duplicate') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item class="delete-action" @click="$emit('delete')" clickable>
|
||||
<v-list-item-icon>
|
||||
<v-icon name="delete" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>{{ t('delete') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
|
||||
<div class="resize-details">
|
||||
@@ -68,6 +91,7 @@ import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'panel',
|
||||
emits: ['update', 'move', 'duplicate', 'delete'],
|
||||
props: {
|
||||
panel: {
|
||||
type: Object as PropType<Panel>,
|
||||
@@ -360,16 +384,17 @@ export default defineComponent({
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.duplicate-icon,
|
||||
.more-icon,
|
||||
.edit-icon,
|
||||
.delete-icon,
|
||||
.note {
|
||||
--v-icon-color: var(--foreground-subdued);
|
||||
--v-icon-color-hover: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
--v-icon-color-hover: var(--danger);
|
||||
.delete-action {
|
||||
--v-list-item-color: var(--danger);
|
||||
--v-list-item-color-hover: var(--danger);
|
||||
--v-list-item-icon-color: var(--danger);
|
||||
}
|
||||
|
||||
.edit-actions {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
:panel="panel"
|
||||
:edit-mode="editMode"
|
||||
@update="$emit('update', { edits: $event, id: panel.id })"
|
||||
@move="$emit('move', panel.id)"
|
||||
@delete="$emit('delete', panel.id)"
|
||||
@duplicate="$emit('duplicate', panel)"
|
||||
/>
|
||||
@@ -20,6 +21,7 @@ import { useElementSize } from '@/composables/use-element-size';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'insights-workspace',
|
||||
emits: ['update', 'move', 'delete', 'duplicate'],
|
||||
components: { InsightsPanel },
|
||||
props: {
|
||||
panels: {
|
||||
|
||||
@@ -63,7 +63,8 @@
|
||||
:panels="panels"
|
||||
:zoom-to-fit="zoomToFit"
|
||||
@update="stagePanelEdits"
|
||||
@delete="confirmDeletePanel = $event"
|
||||
@move="movePanelID = $event"
|
||||
@delete="deletePanel"
|
||||
@duplicate="duplicatePanel"
|
||||
/>
|
||||
|
||||
@@ -75,16 +76,20 @@
|
||||
@cancel="$router.push(`/insights/${primaryKey}`)"
|
||||
/>
|
||||
|
||||
<v-dialog :model-value="!!confirmDeletePanel" @esc="confirmDeletePanel = null">
|
||||
<v-dialog :model-value="!!movePanelID" @update:model-value="movePanelID = null" @esc="movePanelID = null">
|
||||
<v-card>
|
||||
<v-card-title>{{ t('panel_delete_confirm') }}</v-card-title>
|
||||
<v-card-title>{{ t('move_to') }}</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-select :items="movePanelChoices" v-model="movePanelTo" item-text="name" item-value="id" />
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-button @click="confirmDeletePanel = null" secondary>
|
||||
<v-button @click="movePanelID = null" secondary>
|
||||
{{ t('cancel') }}
|
||||
</v-button>
|
||||
<v-button danger @click="deletePanel" :loading="deletingPanel">
|
||||
{{ t('delete') }}
|
||||
<v-button @click="movePanel" :loading="movePanelLoading">
|
||||
{{ t('move') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
@@ -130,9 +135,12 @@ export default defineComponent({
|
||||
const { fullScreen } = toRefs(appStore);
|
||||
|
||||
const editMode = ref(false);
|
||||
const confirmDeletePanel = ref<string | null>(null);
|
||||
const deletingPanel = ref(false);
|
||||
const saving = ref(false);
|
||||
const movePanelLoading = ref(false);
|
||||
|
||||
const movePanelTo = ref(props.primaryKey);
|
||||
|
||||
const movePanelID = ref<string | null>();
|
||||
|
||||
const zoomToFit = ref(false);
|
||||
|
||||
@@ -140,10 +148,17 @@ export default defineComponent({
|
||||
insightsStore.dashboards.find((dashboard) => dashboard.id === props.primaryKey)
|
||||
);
|
||||
|
||||
const movePanelChoices = computed(() => {
|
||||
return insightsStore.dashboards;
|
||||
});
|
||||
|
||||
const stagedPanels = ref<Partial<Panel & { borderRadius: [boolean, boolean, boolean, boolean] }>[]>([]);
|
||||
const panelsToBeDeleted = ref<string[]>([]);
|
||||
|
||||
const panels = computed(() => {
|
||||
const savedPanels = currentDashboard.value?.panels || [];
|
||||
const savedPanels = (currentDashboard.value?.panels || []).filter(
|
||||
(panel) => panelsToBeDeleted.value.includes(panel.id) === false
|
||||
);
|
||||
|
||||
const raw = [
|
||||
...savedPanels.map((panel) => {
|
||||
@@ -212,17 +227,20 @@ export default defineComponent({
|
||||
saving,
|
||||
saveChanges,
|
||||
stageConfiguration,
|
||||
deletingPanel,
|
||||
movePanelID,
|
||||
movePanel,
|
||||
deletePanel,
|
||||
confirmDeletePanel,
|
||||
cancelChanges,
|
||||
duplicatePanel,
|
||||
movePanelLoading,
|
||||
t,
|
||||
toggleFullScreen,
|
||||
zoomToFit,
|
||||
fullScreen,
|
||||
toggleZoomToFit,
|
||||
md,
|
||||
movePanelChoices,
|
||||
movePanelTo,
|
||||
};
|
||||
|
||||
function stagePanelEdits(event: { edits: Partial<Panel>; id?: string }) {
|
||||
@@ -281,6 +299,8 @@ export default defineComponent({
|
||||
panels: updatedPanels,
|
||||
});
|
||||
|
||||
await api.delete(`/panels`, { data: panelsToBeDeleted.value });
|
||||
|
||||
await insightsStore.hydrate();
|
||||
|
||||
stagedPanels.value = [];
|
||||
@@ -292,27 +312,16 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
async function deletePanel() {
|
||||
if (!currentDashboard.value || !confirmDeletePanel.value) return;
|
||||
async function deletePanel(id: string) {
|
||||
if (!currentDashboard.value) return;
|
||||
|
||||
deletingPanel.value = true;
|
||||
|
||||
try {
|
||||
if (confirmDeletePanel.value.startsWith('_') === false) {
|
||||
await api.delete(`/panels/${confirmDeletePanel.value}`);
|
||||
await insightsStore.hydrate();
|
||||
}
|
||||
stagedPanels.value = stagedPanels.value.filter((panel) => panel.id !== confirmDeletePanel.value);
|
||||
confirmDeletePanel.value = null;
|
||||
} catch (err) {
|
||||
unexpectedError(err);
|
||||
} finally {
|
||||
deletingPanel.value = false;
|
||||
}
|
||||
stagedPanels.value = stagedPanels.value.filter((panel) => panel.id !== id);
|
||||
if (id.startsWith('_') === false) panelsToBeDeleted.value.push(id);
|
||||
}
|
||||
|
||||
function cancelChanges() {
|
||||
stagedPanels.value = [];
|
||||
panelsToBeDeleted.value = [];
|
||||
editMode.value = false;
|
||||
}
|
||||
|
||||
@@ -330,6 +339,26 @@ export default defineComponent({
|
||||
function toggleZoomToFit() {
|
||||
zoomToFit.value = !zoomToFit.value;
|
||||
}
|
||||
|
||||
async function movePanel() {
|
||||
movePanelLoading.value = true;
|
||||
|
||||
try {
|
||||
await api.patch(`/panels/${movePanelID.value}`, {
|
||||
dashboard: movePanelTo.value,
|
||||
});
|
||||
|
||||
stagedPanels.value = stagedPanels.value.filter((panel) => panel.id !== movePanelID.value);
|
||||
|
||||
await insightsStore.hydrate();
|
||||
|
||||
movePanelID.value = null;
|
||||
} catch (err) {
|
||||
unexpectedError(err);
|
||||
} finally {
|
||||
movePanelLoading.value = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -75,7 +75,7 @@ export default defineComponent({
|
||||
if (!metric.value) return null;
|
||||
|
||||
if (props.options.abbreviate) {
|
||||
return abbreviateNumber(metric.value);
|
||||
return abbreviateNumber(metric.value, props.options.decimals);
|
||||
}
|
||||
|
||||
return n(Number(metric.value), 'decimal', {
|
||||
|
||||
@@ -17,10 +17,22 @@ export default definePanel({
|
||||
width: 'half',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'color',
|
||||
name: '$t:color',
|
||||
type: 'string',
|
||||
schema: {
|
||||
default_value: '#00C897',
|
||||
},
|
||||
meta: {
|
||||
interface: 'select-color',
|
||||
width: 'half',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'function',
|
||||
type: 'string',
|
||||
name: '$t:aggregate_function',
|
||||
name: '$t:group_aggregation',
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'select-dropdown',
|
||||
@@ -62,6 +74,46 @@ export default definePanel({
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'precision',
|
||||
type: 'string',
|
||||
name: '$t:group_precision',
|
||||
meta: {
|
||||
interface: 'select-dropdown',
|
||||
width: 'half',
|
||||
options: {
|
||||
choices: [
|
||||
{
|
||||
text: 'Second',
|
||||
value: 'second',
|
||||
},
|
||||
{
|
||||
text: 'Minute',
|
||||
value: 'minute',
|
||||
},
|
||||
{
|
||||
text: 'Hour',
|
||||
value: 'hour',
|
||||
},
|
||||
{
|
||||
text: 'Day',
|
||||
value: 'day',
|
||||
},
|
||||
{
|
||||
text: 'Month',
|
||||
value: 'month',
|
||||
},
|
||||
{
|
||||
text: 'Year',
|
||||
value: 'year',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
default_value: 'hour',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'dateField',
|
||||
type: 'string',
|
||||
@@ -75,19 +127,6 @@ export default definePanel({
|
||||
width: 'half',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'valueField',
|
||||
type: 'string',
|
||||
name: '$t:panels.time_series.value_field',
|
||||
meta: {
|
||||
interface: 'system-field',
|
||||
options: {
|
||||
collectionField: 'collection',
|
||||
typeAllowList: ['integer', 'bigInteger', 'float', 'decimal'],
|
||||
},
|
||||
width: 'half',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'range',
|
||||
type: 'dropdown',
|
||||
@@ -145,10 +184,23 @@ export default definePanel({
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'valueField',
|
||||
type: 'string',
|
||||
name: '$t:panels.time_series.value_field',
|
||||
meta: {
|
||||
interface: 'system-field',
|
||||
options: {
|
||||
collectionField: 'collection',
|
||||
typeAllowList: ['integer', 'bigInteger', 'float', 'decimal'],
|
||||
},
|
||||
width: 'half',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'decimals',
|
||||
type: 'integer',
|
||||
name: '$t:decimals',
|
||||
name: '$t:value_decimals',
|
||||
meta: {
|
||||
interface: 'input',
|
||||
width: 'half',
|
||||
@@ -161,64 +213,27 @@ export default definePanel({
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'precision',
|
||||
type: 'string',
|
||||
name: '$t:precision',
|
||||
field: 'min',
|
||||
type: 'integer',
|
||||
name: '$t:min_value',
|
||||
meta: {
|
||||
interface: 'select-dropdown',
|
||||
interface: 'input',
|
||||
width: 'half',
|
||||
options: {
|
||||
choices: [
|
||||
{
|
||||
text: 'Second',
|
||||
value: 'second',
|
||||
},
|
||||
{
|
||||
text: 'Minute',
|
||||
value: 'minute',
|
||||
},
|
||||
{
|
||||
text: 'Hour',
|
||||
value: 'hour',
|
||||
},
|
||||
{
|
||||
text: 'Day',
|
||||
value: 'day',
|
||||
},
|
||||
{
|
||||
text: 'Month',
|
||||
value: 'month',
|
||||
},
|
||||
{
|
||||
text: 'Year',
|
||||
value: 'year',
|
||||
},
|
||||
],
|
||||
placeholder: '$t:automatic',
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
default_value: 'hour',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'showZero',
|
||||
name: '$t:show_zero',
|
||||
type: 'boolean',
|
||||
field: 'max',
|
||||
type: 'integer',
|
||||
name: '$t:max_value',
|
||||
meta: {
|
||||
interface: 'boolean',
|
||||
width: 'half',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'color',
|
||||
name: '$t:color',
|
||||
type: 'string',
|
||||
schema: {
|
||||
default_value: '#00C897',
|
||||
},
|
||||
meta: {
|
||||
interface: 'select-color',
|
||||
interface: 'input',
|
||||
width: 'half',
|
||||
options: {
|
||||
placeholder: '$t:automatic',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useFieldsStore } from '@/stores';
|
||||
import { Filter } from '@/types';
|
||||
import { abbreviateNumber } from '@/utils/abbreviate-number';
|
||||
|
||||
type TimeSeriesOptions = {
|
||||
collection: string;
|
||||
@@ -21,7 +22,8 @@ type TimeSeriesOptions = {
|
||||
color: string;
|
||||
decimals: number;
|
||||
precision: 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second';
|
||||
showZero: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
filter: Filter | null;
|
||||
};
|
||||
|
||||
@@ -58,9 +60,9 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => props.options, () => props.show_header],
|
||||
(newOptions, oldOptions) => {
|
||||
if (isEqual(newOptions, oldOptions) === false) {
|
||||
[() => props.options, () => props.show_header, () => props.height],
|
||||
(newVal, oldVal) => {
|
||||
if (isEqual(newVal, oldVal) === false) {
|
||||
fetchData();
|
||||
chart.value?.destroy();
|
||||
setupChart();
|
||||
@@ -262,7 +264,14 @@ export default defineComponent({
|
||||
},
|
||||
yaxis: {
|
||||
forceNiceScale: true,
|
||||
min: props.options.showZero ? 0 : undefined,
|
||||
min: props.options.min ? Number(props.options.min) : undefined,
|
||||
max: props.options.max ? Number(props.options.max) : undefined,
|
||||
tickAmount: props.height - 2,
|
||||
labels: {
|
||||
formatter: (value: number) => {
|
||||
return value > 10000 ? abbreviateNumber(value, 1) : n(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
export function abbreviateNumber(value: number): number | string {
|
||||
if (value >= 1000) {
|
||||
value = Math.round(value);
|
||||
export function abbreviateNumber(
|
||||
number: number,
|
||||
decimalPlaces = 0,
|
||||
units: string[] = ['K', 'M', 'B', 'T']
|
||||
): number | string {
|
||||
const isNegative = number < 0;
|
||||
|
||||
const suffixes = ['', 'K', 'M', 'B', 'T'];
|
||||
const suffixNum = Math.floor(('' + value).length / 3);
|
||||
let shortValue: number = value;
|
||||
number = Math.abs(number);
|
||||
|
||||
for (let precision = 2; precision >= 1; precision--) {
|
||||
shortValue = parseFloat((suffixNum != 0 ? value / Math.pow(1000, suffixNum) : value).toPrecision(precision));
|
||||
const dotLessShortValue = (shortValue + '').replace(/[^a-zA-Z 0-9]+/g, '');
|
||||
if (dotLessShortValue.length <= 2) break;
|
||||
let stringValue = String(number);
|
||||
|
||||
if (number >= 1000) {
|
||||
const precisionScale = Math.pow(10, decimalPlaces);
|
||||
|
||||
for (let i = units.length - 1; i >= 0; i--) {
|
||||
const size = Math.pow(10, (i + 1) * 3);
|
||||
|
||||
if (size <= number) {
|
||||
number = Math.round((number * precisionScale) / size) / precisionScale;
|
||||
|
||||
if (number === 1000 && i < units.length - 1) {
|
||||
number = 1;
|
||||
i++;
|
||||
}
|
||||
|
||||
stringValue = number + units[i];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let valueAsString = String(shortValue);
|
||||
|
||||
if (shortValue % 1 !== 0) {
|
||||
valueAsString = shortValue.toFixed(1);
|
||||
}
|
||||
|
||||
return valueAsString + suffixes[suffixNum];
|
||||
}
|
||||
|
||||
return value;
|
||||
if (isNegative) {
|
||||
stringValue = `-${stringValue}`;
|
||||
}
|
||||
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user