From 05c34471739e3a89a677c6699d41de63824cda52 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Sun, 20 Jun 2021 19:11:16 -0400 Subject: [PATCH] Add "move" option, various tweaks --- app/src/lang/translations/en-US.yaml | 8 + app/src/modules/insights/components/panel.vue | 49 ++++-- .../modules/insights/components/workspace.vue | 2 + app/src/modules/insights/routes/dashboard.vue | 83 ++++++---- app/src/panels/metric/metric.vue | 2 +- app/src/panels/time-series/index.ts | 143 ++++++++++-------- app/src/panels/time-series/time-series.vue | 19 ++- app/src/utils/abbreviate-number.ts | 51 ++++--- 8 files changed, 229 insertions(+), 128 deletions(-) diff --git a/app/src/lang/translations/en-US.yaml b/app/src/lang/translations/en-US.yaml index 6a67168448..24305bec4e 100644 --- a/app/src/lang/translations/en-US.yaml +++ b/app/src/lang/translations/en-US.yaml @@ -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... diff --git a/app/src/modules/insights/components/panel.vue b/app/src/modules/insights/components/panel.vue index 64d2021a5f..eba4d39260 100644 --- a/app/src/modules/insights/components/panel.vue +++ b/app/src/modules/insights/components/panel.vue @@ -21,13 +21,6 @@
- - + + + + + + + + + + + {{ t('move_to') }} + + + + + + + + {{ t('duplicate') }} + + + + + + + {{ t('delete') }} + + +
@@ -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, @@ -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 { diff --git a/app/src/modules/insights/components/workspace.vue b/app/src/modules/insights/components/workspace.vue index db919beadc..318bbfc910 100644 --- a/app/src/modules/insights/components/workspace.vue +++ b/app/src/modules/insights/components/workspace.vue @@ -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: { diff --git a/app/src/modules/insights/routes/dashboard.vue b/app/src/modules/insights/routes/dashboard.vue index f7423b848a..b1192af643 100644 --- a/app/src/modules/insights/routes/dashboard.vue +++ b/app/src/modules/insights/routes/dashboard.vue @@ -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}`)" /> - + - {{ t('panel_delete_confirm') }} + {{ t('move_to') }} + + + + - + {{ t('cancel') }} - - {{ t('delete') }} + + {{ t('move') }} @@ -130,9 +135,12 @@ export default defineComponent({ const { fullScreen } = toRefs(appStore); const editMode = ref(false); - const confirmDeletePanel = ref(null); - const deletingPanel = ref(false); const saving = ref(false); + const movePanelLoading = ref(false); + + const movePanelTo = ref(props.primaryKey); + + const movePanelID = ref(); 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[]>([]); + const panelsToBeDeleted = ref([]); 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; 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; + } + } }, }); diff --git a/app/src/panels/metric/metric.vue b/app/src/panels/metric/metric.vue index 5025f95191..6ae265f7dc 100644 --- a/app/src/panels/metric/metric.vue +++ b/app/src/panels/metric/metric.vue @@ -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', { diff --git a/app/src/panels/time-series/index.ts b/app/src/panels/time-series/index.ts index dbc2e2204c..a57d7ea790 100644 --- a/app/src/panels/time-series/index.ts +++ b/app/src/panels/time-series/index.ts @@ -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', + }, }, }, { diff --git a/app/src/panels/time-series/time-series.vue b/app/src/panels/time-series/time-series.vue index 78a84fcc4e..314f13b2ef 100644 --- a/app/src/panels/time-series/time-series.vue +++ b/app/src/panels/time-series/time-series.vue @@ -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); + }, + }, }, }); diff --git a/app/src/utils/abbreviate-number.ts b/app/src/utils/abbreviate-number.ts index 08479f49c0..d93b8df7c5 100644 --- a/app/src/utils/abbreviate-number.ts +++ b/app/src/utils/abbreviate-number.ts @@ -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; }