Add "move" option, various tweaks

This commit is contained in:
rijkvanzanten
2021-06-20 19:11:16 -04:00
parent 3021a7d336
commit 05c3447173
8 changed files with 229 additions and 128 deletions

View File

@@ -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...

View File

@@ -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 {

View File

@@ -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: {

View File

@@ -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>

View File

@@ -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', {

View File

@@ -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',
},
},
},
{

View File

@@ -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);
},
},
},
});

View File

@@ -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;
}