Files
akvorado/console/frontend/src/views/VisualizePage/OptionsPanel.vue
Vincent Bernat 52816e7fe1 console/frontend: convert from JavaScript to TypeScript
I thought it would be easier!

TypeScript typing is a bit different than Python one. Mostly, I think
that because JavaScript did not have named arguments, libs tried to be
smart by allowing polymorph arguments, the typing system has to be
powerful enough to handle complex conditions. However, sometimes, it's
quite complex to express things and sometimes, this can be a bit
buggy (many issues on GitHub about that). For example, you can narrow a
type with "if" and "switch", but for the later, you can't have several
cases for the same body. However, the ternary operator does not allow to
narrow the type. Moreover, in functional programming, lambdas are often
used but because TypeScript is not smart enough to know if a callback is
executed immediatly, it cannot assume the previous narrowing of a type
is still valid inside the lambda.

Also, eCharts typing is quite difficult because it accepts objects with
many different types. Without TypeScript, you know that you will get one
specific form, but with TypeScript, you need to either force or
enumerate cases you won't have. Moreover, the types are not all exposed,
so you have to dig them inside the `d.ts` files.

Another difficulty is that Vue 3 LSP server ("volar") does not work like
the one for generic TypeScript. In Emacs, it does not provide
information automatically in the modeline. Also, I did not find a way to
expand a type. Sometimes, it just says that a variable is
SomeRandomType, but you need to lookup yourself what could be
SomeRandomType. When there are errors, TypeScript does a good job to
explain where the incompatibility is. However, the errors are quite
verbose and the end is the most interesting hint. I suppose the LSP
server could have a condensed version of the error message ("options →
tooltip → formatter should be type X, not type Y") to help.

Emit signals are also not typed on the destination component. I don't
know if this is expected.

The migration has two benefits:

1. the code should be more robust (still not tested, but types are a
   great help to catch problems)

2. the values sent from one component to another are now correctly
   specified (previously, `console.log` was heavily used to check that we
   get what we want) and I can use `null` when I don't have a value instead
   of using an empty object and putting question marks everywhere
2022-11-23 12:12:54 +01:00

276 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- SPDX-FileCopyrightText: 2022 Free Mobile -->
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
<template>
<aside
class="transition-height transition-width w-full shrink-0 duration-100 lg:h-auto"
:class="open ? 'h-80 lg:w-72' : 'h-4 lg:w-4'"
>
<span
class="absolute z-40 translate-x-4 transition-transform lg:translate-y-4"
:class="
open
? 'translate-y-80 rotate-180 lg:translate-x-72'
: 'translate-y-4 lg:translate-x-0'
"
>
<button
class="flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-white shadow transition-transform duration-100 hover:bg-gray-300 dark:bg-gray-900 dark:shadow-white/10 dark:hover:bg-black lg:translate-x-1/2 lg:translate-y-0"
:class="open ? 'translate-y-1/2' : '-translate-y-1/2'"
@click="open = !open"
>
<ChevronRightIcon class="hidden lg:inline" />
<ChevronDownIcon class="lg:hidden" />
</button>
</span>
<form
class="h-full overflow-y-auto border-b border-gray-300 bg-gray-100 dark:border-slate-700 dark:bg-slate-800 lg:border-r lg:border-b-0"
autocomplete="off"
spellcheck="false"
@submit.prevent="submitOptions()"
>
<div v-if="open" class="flex flex-col px-3 py-4 lg:max-h-screen">
<div
class="mb-2 flex flex-row flex-wrap items-center justify-between gap-2 sm:max-lg:flex-nowrap"
>
<InputButton
attr-type="submit"
:disabled="hasErrors && !loading"
:loading="loading"
:type="loading ? 'alternative' : 'primary'"
class="order-2 w-28 justify-center sm:max-lg:order-4"
>{{ loading ? "Cancel" : applyLabel }}</InputButton
>
<InputChoice
v-model="units"
:choices="[
{ label: 'L3ᵇₛ', name: 'l3bps' },
{ label: 'L2ᵇₛ', name: 'l2bps' },
{ label: 'ᵖ⁄ₛ', name: 'pps' },
]"
label="Unit"
class="order-1"
/>
<InputListBox
v-model="graphType"
:items="graphTypeList"
class="order-3 grow basis-full sm:max-lg:order-3 sm:max-lg:basis-0"
label="Graph type"
>
<template #selected>{{ graphType.name }}</template>
<template #item="{ name }">
<div class="flex w-full items-center justify-between">
<span>{{ name }}</span>
<GraphIcon
:name="name"
class="mr-1 inline h-4 text-gray-500 dark:text-gray-400"
/>
</div>
</template>
</InputListBox>
<div
class="order-4 flex grow flex-row justify-between gap-x-3 sm:max-lg:order-2 sm:max-lg:grow-0 sm:max-lg:flex-col"
>
<InputCheckbox
v-if="
graphType.type === 'stacked' ||
graphType.type === 'lines' ||
graphType.type === 'grid'
"
v-model="bidirectional"
label="Bidirectional"
/>
<InputCheckbox
v-if="graphType.type === 'stacked'"
v-model="previousPeriod"
label="Previous period"
/>
</div>
</div>
<SectionLabel>Time range</SectionLabel>
<InputTimeRange v-model="timeRange" />
<SectionLabel>Dimensions</SectionLabel>
<InputDimensions
v-model="dimensions"
:min-dimensions="graphType.name === graphTypes.sankey ? 2 : 0"
/>
<SectionLabel>
<template #default>Filter</template>
<template #hint>
<kbd
class="rounded border border-gray-300 bg-gray-200 px-1 dark:border-gray-600 dark:bg-gray-900"
>Ctrl-Space</kbd
>
for completions
</template>
</SectionLabel>
<InputFilter v-model="filter" class="mb-2" @submit="submitOptions()" />
</div>
</form>
</aside>
</template>
<script lang="ts" setup>
import { ref, watch, computed, inject, toRaw } from "vue";
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/vue/solid";
import {
default as InputTimeRange,
type ModelType as InputTimeRangeModelType,
} from "@/components/InputTimeRange.vue";
import {
default as InputDimensions,
type ModelType as InputDimensionsModelType,
} from "@/components/InputDimensions.vue";
import InputListBox from "@/components/InputListBox.vue";
import InputButton from "@/components/InputButton.vue";
import InputCheckbox from "@/components/InputCheckbox.vue";
import InputChoice from "@/components/InputChoice.vue";
import {
default as InputFilter,
type ModelType as InputFilterModelType,
} from "@/components/InputFilter.vue";
import { ServerConfigKey } from "@/components/ServerConfigProvider.vue";
import SectionLabel from "./SectionLabel.vue";
import GraphIcon from "./GraphIcon.vue";
import type { Units } from ".";
import { isEqual } from "lodash-es";
const props = withDefaults(
defineProps<{
modelValue: ModelType;
loading?: boolean;
}>(),
{
loading: false,
}
);
const emit = defineEmits<{
(e: "update:modelValue", value: typeof props.modelValue): void;
(e: "cancel"): void;
}>();
const graphTypeList = Object.entries(graphTypes).map(([k, v], idx) => ({
id: idx + 1,
type: k as keyof typeof graphTypes, // why isn't it infered?
name: v,
}));
const open = ref(false);
const graphType = ref(graphTypeList[0]);
const timeRange = ref<InputTimeRangeModelType>(null);
const dimensions = ref<InputDimensionsModelType>(null);
const filter = ref<InputFilterModelType>(null);
const units = ref<Units>("l3bps");
const bidirectional = ref(false);
const previousPeriod = ref(false);
const submitOptions = () => {
if (props.loading) {
emit("cancel");
} else {
emit("update:modelValue", options.value);
}
};
const options = computed((): ModelType => {
if (!timeRange.value || !dimensions.value || !filter.value) {
return options.value;
}
return {
graphType: graphType.value.type,
start: timeRange.value?.start,
end: timeRange.value?.end,
dimensions: dimensions.value?.selected,
limit: dimensions.value?.limit,
filter: filter.value?.expression,
units: units.value,
bidirectional: false,
previousPeriod: false,
// Depending on the graph type...
...(graphType.value.type === "stacked" && {
bidirectional: bidirectional.value,
previousPeriod: previousPeriod.value,
}),
...(graphType.value.type === "lines" && {
bidirectional: bidirectional.value,
}),
...(graphType.value.type === "grid" && {
bidirectional: bidirectional.value,
}),
};
});
const applyLabel = computed(() =>
isEqual(options.value, props.modelValue) ? "Refresh" : "Apply"
);
const hasErrors = computed(
() =>
!!(
timeRange.value?.errors ||
dimensions.value?.errors ||
filter.value?.errors
)
);
const serverConfiguration = inject(ServerConfigKey)!;
watch(
() =>
[
props.modelValue,
serverConfiguration.value?.defaultVisualizeOptions,
] as const,
([modelValue, defaultOptions]) => {
if (!defaultOptions) return;
const currentValue: NonNullable<ModelType> = modelValue ?? {
graphType: "stacked",
start: defaultOptions.start,
end: defaultOptions.end,
dimensions: toRaw(defaultOptions.dimensions),
limit: 10,
filter: defaultOptions.filter,
units: "l3bps",
bidirectional: false,
previousPeriod: false,
};
// Dispatch values in refs
const t = currentValue.graphType;
graphType.value =
graphTypeList.find(({ type }) => type === t) || graphTypeList[0];
timeRange.value = { start: currentValue.start, end: currentValue.end };
dimensions.value = {
selected: [...currentValue.dimensions],
limit: currentValue.limit,
};
filter.value = { expression: currentValue.filter };
units.value = currentValue.units;
bidirectional.value = currentValue.bidirectional;
previousPeriod.value = currentValue.previousPeriod;
// A bit risky, but it seems to work.
if (!isEqual(modelValue, options.value)) {
open.value = true;
if (!hasErrors.value) {
emit("update:modelValue", options.value);
}
}
},
{ immediate: true, deep: true }
);
</script>
<script lang="ts">
import { graphTypes } from "./constants";
export type ModelType = {
graphType: keyof typeof graphTypes;
start: string;
end: string;
dimensions: string[];
limit: number;
filter: string;
units: Units;
bidirectional: boolean;
previousPeriod: boolean;
} | null;
</script>