mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 10:59:20 +02:00
update metric simulation styles
This commit is contained in:
parent
361605c93d
commit
7467e1eca2
|
|
@ -348,6 +348,8 @@ test context).
|
|||
functions.
|
||||
- Tailwind utilities by default; custom rules go in `main.css` and are
|
||||
applied by class. No `<style>` blocks or static inline `style=`.
|
||||
- Lengths are rem or screen proportions (`dvh`, `dvw`, `%`), never `px`,
|
||||
including in arbitrary values: `max-h-[min(90dvh,60rem)]`.
|
||||
|
||||
## PromQL grammar
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
makeEditor,
|
||||
makeNode,
|
||||
mountNodeView,
|
||||
mountNodeViewUnderTooltipProvider,
|
||||
} from "../../test-helpers/node-view"
|
||||
import { DiffStatus } from "~/components/editor/diff/position-map"
|
||||
import {
|
||||
|
|
@ -20,7 +21,7 @@ import {
|
|||
disposeMockEndpoints,
|
||||
makeXid,
|
||||
} from "~/composables/api/test-helpers"
|
||||
import { emitFrom } from "~/components/test-helpers"
|
||||
import { emitFrom, renderedIconNames } from "~/components/test-helpers"
|
||||
|
||||
const DOCUMENT_ID = makeXid("doc")
|
||||
const BRANCH_ID = makeXid("branch")
|
||||
|
|
@ -77,11 +78,15 @@ function mountBlock(
|
|||
getPos?: () => number | undefined
|
||||
nodeAtPos?: { attrs: Record<string, unknown> } | null
|
||||
editing?: { uid: string; name: string; color: string } | null
|
||||
underTooltipProvider?: boolean
|
||||
} = {},
|
||||
) {
|
||||
const uid = options.uid ?? nextUid()
|
||||
const mount = options.underTooltipProvider
|
||||
? mountNodeViewUnderTooltipProvider
|
||||
: mountNodeView
|
||||
|
||||
return mountNodeView(MainBlock, {
|
||||
return mount(MainBlock, {
|
||||
node: makeNode({ uid: uid, ...options.attrs }),
|
||||
editor: makeMetricEditor({
|
||||
nodeAtPos:
|
||||
|
|
@ -205,6 +210,18 @@ describe("<MetricMainBlock>", { concurrent: false }, () => {
|
|||
expect(storedConfig(uid)?.axisBounds).toEqual({ min: 0, max: 10 })
|
||||
})
|
||||
|
||||
it("reads a block saved without a chart type as a time series", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const uid = nextUid()
|
||||
|
||||
await mountBlock({ uid: uid, attrs: { visualizationType: null } })
|
||||
|
||||
expect(storedConfig(uid)?.visualizationType).toBe(
|
||||
GenericQueryChartType.Line,
|
||||
)
|
||||
})
|
||||
|
||||
it("publishes nothing for a block shown inside a diff", async ({
|
||||
expect,
|
||||
}) => {
|
||||
|
|
@ -298,6 +315,7 @@ describe("<MetricMainBlock>", { concurrent: false }, () => {
|
|||
await mountBlock({
|
||||
uid: uid,
|
||||
attrs: { simulationPreset: MetricSimulationPreset.DiskUsage },
|
||||
underTooltipProvider: true,
|
||||
})
|
||||
|
||||
expect(storedConfig(uid)?.simulationPreset).toBe(
|
||||
|
|
@ -305,6 +323,28 @@ describe("<MetricMainBlock>", { concurrent: false }, () => {
|
|||
)
|
||||
})
|
||||
|
||||
it("shows the simulation icon while the visualization simulates", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const wrapper = await mountBlock({ underTooltipProvider: true })
|
||||
|
||||
emitFrom(wrapper, VisualizationContainer, "simulation-visible", true)
|
||||
await nextTick()
|
||||
|
||||
expect(renderedIconNames(wrapper)).toContain("mingcute:test-tube-line")
|
||||
})
|
||||
|
||||
it("hides the simulation icon while the visualization draws real data", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const wrapper = await mountBlock()
|
||||
|
||||
emitFrom(wrapper, VisualizationContainer, "simulation-visible", false)
|
||||
await nextTick()
|
||||
|
||||
expect(renderedIconNames(wrapper)).not.toContain("mingcute:test-tube-line")
|
||||
})
|
||||
|
||||
it("flattens a legacy config on the first edit", async ({ expect }) => {
|
||||
const updateAttributes = vi.fn()
|
||||
const uid = nextUid()
|
||||
|
|
|
|||
|
|
@ -73,7 +73,8 @@ function migrateLegacyIfNeeded(fieldAttrs: Record<string, any>) {
|
|||
safeUpdateAttributes({
|
||||
title: legacy.title ?? "",
|
||||
dataSourceId: legacy.dataSourceId ?? null,
|
||||
visualizationType: legacy.type ?? legacy.visualizationType ?? null,
|
||||
visualizationType:
|
||||
legacy.type ?? legacy.visualizationType ?? GenericQueryChartType.Line,
|
||||
queries: legacy.queries ?? null,
|
||||
timeRange: legacy.timeRange ?? null,
|
||||
refreshInterval: legacy.refreshInterval ?? null,
|
||||
|
|
@ -123,9 +124,9 @@ const config = reactive({
|
|||
get: () =>
|
||||
legacyConfig.value?.type ??
|
||||
legacyConfig.value?.visualizationType ??
|
||||
(props.node.attrs
|
||||
.visualizationType as MetricConfig["visualizationType"]) ??
|
||||
null,
|
||||
(props.node.attrs.visualizationType as
|
||||
MetricConfig["visualizationType"] | null) ??
|
||||
GenericQueryChartType.Line,
|
||||
set: (v) => {
|
||||
migrateLegacyIfNeeded({ visualizationType: v })
|
||||
},
|
||||
|
|
@ -243,6 +244,7 @@ const lastOtherEditingUser = computed(() => {
|
|||
})
|
||||
|
||||
const disableVisualizationRefresh = ref(false)
|
||||
const simulationVisible = ref(false)
|
||||
|
||||
watchImmediate(
|
||||
[
|
||||
|
|
@ -346,7 +348,20 @@ function disableVisualizationRefreshTemporarily() {
|
|||
>
|
||||
{{ lastOtherEditingUser.name }}
|
||||
</div>
|
||||
<div class="absolute top-1 right-1.5 z-1 flex items-center gap-1">
|
||||
<div
|
||||
class="absolute top-1 right-1.5 z-1 flex items-center rounded-md bg-background pl-0.5"
|
||||
>
|
||||
<ShadcnUiTooltip v-if="simulationVisible">
|
||||
<ShadcnUiTooltipTrigger as-child>
|
||||
<Icon
|
||||
name="mingcute:test-tube-line"
|
||||
class="size-3.5 text-foreground"
|
||||
/>
|
||||
</ShadcnUiTooltipTrigger>
|
||||
<ShadcnUiTooltipContent side="bottom">
|
||||
{{ $t("editor.metrics.simulation.tooltip") }}
|
||||
</ShadcnUiTooltipContent>
|
||||
</ShadcnUiTooltip>
|
||||
<ShadcnUiPopoverTrigger as-child>
|
||||
<ShadcnUiButton
|
||||
variant="ghost-plain"
|
||||
|
|
@ -362,6 +377,7 @@ function disableVisualizationRefreshTemporarily() {
|
|||
:uid="props.node.attrs.uid"
|
||||
:disable-refresh="disableVisualizationRefresh"
|
||||
@simulate="(preset) => (config.simulationPreset = preset)"
|
||||
@simulation-visible="(v) => (simulationVisible = v)"
|
||||
/>
|
||||
</div>
|
||||
<ShadcnUiPopoverContent class="w-50" side="right" side-flip align="start">
|
||||
|
|
|
|||
|
|
@ -95,7 +95,9 @@ function openModal() {
|
|||
size="2sm"
|
||||
@click.stop="openModal"
|
||||
>
|
||||
<Icon name="lucide:square-pen" />
|
||||
<Icon
|
||||
:name="isEditingDisabled ? 'mingcute:eye-line' : 'mingcute:pencil-line'"
|
||||
/>
|
||||
{{
|
||||
!isEditingDisabled
|
||||
? $t("editor.metrics.config.modal-trigger-button-normal")
|
||||
|
|
|
|||
|
|
@ -109,54 +109,7 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
|
||||
afterEach(disposeMockEndpoints)
|
||||
|
||||
it("asks for a visualization type before anything else", async ({
|
||||
expect,
|
||||
}) => {
|
||||
const wrapper = await mountContainer(
|
||||
metricConfig({ visualizationType: null }),
|
||||
)
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
t("editor.metrics.status.type-not-selected.title"),
|
||||
)
|
||||
expect(wrapper.text()).toContain(
|
||||
t("editor.metrics.status.type-not-selected.normal-action-button"),
|
||||
)
|
||||
})
|
||||
|
||||
it("offers only to view the metrics in read mode", async ({ expect }) => {
|
||||
useEditorMeta().setEditable(false)
|
||||
|
||||
const wrapper = await mountContainer(
|
||||
metricConfig({ visualizationType: null }),
|
||||
)
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
t("editor.metrics.status.type-not-selected.readonly-action-button"),
|
||||
)
|
||||
})
|
||||
|
||||
it("hides the action button when the host asks it to", async ({ expect }) => {
|
||||
const wrapper = await mountContainer(
|
||||
metricConfig({ visualizationType: null }),
|
||||
{ hideEmptyActionButton: true },
|
||||
)
|
||||
|
||||
expect(wrapper.findAll("button")).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("shows a bare placeholder in a simplified host", async ({ expect }) => {
|
||||
const wrapper = await mountContainer(
|
||||
metricConfig({ visualizationType: null }),
|
||||
{ simplifiedEmpty: true },
|
||||
)
|
||||
|
||||
expect(wrapper.text()).toBe(
|
||||
t("editor.metrics.status.simplified-config-in-progress.title"),
|
||||
)
|
||||
})
|
||||
|
||||
it("asks for a data source once a type is chosen", async ({ expect }) => {
|
||||
it("asks for a data source before anything else", async ({ expect }) => {
|
||||
const wrapper = await mountContainer(
|
||||
metricConfig({ dataSourceId: null, queries: [] }),
|
||||
)
|
||||
|
|
@ -164,6 +117,32 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
expect(wrapper.text()).toContain(
|
||||
t("editor.metrics.status.data-source-not-selected.title"),
|
||||
)
|
||||
expect(wrapper.text()).toContain(
|
||||
t("editor.metrics.status.data-source-not-selected.normal-action-button"),
|
||||
)
|
||||
})
|
||||
|
||||
it("offers only to view the metrics in read mode", async ({ expect }) => {
|
||||
useEditorMeta().setEditable(false)
|
||||
|
||||
const wrapper = await mountContainer(
|
||||
metricConfig({ dataSourceId: null, queries: [] }),
|
||||
)
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
t(
|
||||
"editor.metrics.status.data-source-not-selected.readonly-action-button",
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it("hides the action button when the host asks it to", async ({ expect }) => {
|
||||
const wrapper = await mountContainer(
|
||||
metricConfig({ dataSourceId: null, queries: [] }),
|
||||
{ hideEmptyActionButton: true },
|
||||
)
|
||||
|
||||
expect(wrapper.findAll("button")).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("shows a bare placeholder for a missing data source in a simplified host", async ({
|
||||
|
|
@ -319,13 +298,13 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
it("opens the config modal from the action button", async ({ expect }) => {
|
||||
const uid = nextUid()
|
||||
const wrapper = await mountContainer(
|
||||
metricConfig({ visualizationType: null }),
|
||||
metricConfig({ dataSourceId: null, queries: [] }),
|
||||
{ uid: uid },
|
||||
)
|
||||
|
||||
await findButtonByText(
|
||||
wrapper,
|
||||
t("editor.metrics.status.type-not-selected.normal-action-button"),
|
||||
t("editor.metrics.status.data-source-not-selected.normal-action-button"),
|
||||
).trigger("click")
|
||||
|
||||
expect(useEditorStore().activeMetricBlockConfig).toBe(uid)
|
||||
|
|
@ -512,7 +491,7 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
await vi.waitFor(() => {
|
||||
expect(calls.length).toBeGreaterThan(0)
|
||||
}, WAIT_FOR_OPTIONS)
|
||||
expect(wrapper.text()).not.toContain(t("editor.metrics.simulation.label"))
|
||||
expect(simulationVisible(wrapper)).toBe(false)
|
||||
})
|
||||
|
||||
// a simulation stands in for a metric that has not arrived, not
|
||||
|
|
@ -535,9 +514,7 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(wrapper.text()).not.toContain(
|
||||
t("editor.metrics.simulation.label"),
|
||||
)
|
||||
expect(simulationVisible(wrapper)).toBe(false)
|
||||
}, WAIT_FOR_OPTIONS)
|
||||
},
|
||||
)
|
||||
|
|
@ -552,7 +529,7 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
metricConfig({ simulationPreset: MetricSimulationPreset.CPUUsage }),
|
||||
)
|
||||
|
||||
expect(wrapper.text()).toContain(t("editor.metrics.simulation.label"))
|
||||
expect(simulationVisible(wrapper)).toBe(true)
|
||||
})
|
||||
|
||||
it("draws the window the block is configured for", async ({ expect }) => {
|
||||
|
|
@ -600,7 +577,7 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
},
|
||||
)
|
||||
|
||||
it("marks a simulated block for every reader", async ({ expect }) => {
|
||||
it("reports a simulated block for every reader", async ({ expect }) => {
|
||||
useEditorMeta().setEditable(false)
|
||||
mockEndpoint("GET", QUERY_URL, () => seriesResult())
|
||||
|
||||
|
|
@ -608,15 +585,15 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
metricConfig({ simulationPreset: MetricSimulationPreset.CPUUsage }),
|
||||
)
|
||||
|
||||
expect(wrapper.text()).toContain(t("editor.metrics.simulation.label"))
|
||||
expect(simulationVisible(wrapper)).toBe(true)
|
||||
})
|
||||
|
||||
it("leaves an unsimulated block unmarked", async ({ expect }) => {
|
||||
it("reports an unsimulated block as not simulating", async ({ expect }) => {
|
||||
mockEndpoint("GET", QUERY_URL, () => seriesResult())
|
||||
|
||||
const wrapper = await mountContainer(metricConfig())
|
||||
|
||||
expect(wrapper.text()).not.toContain(t("editor.metrics.simulation.label"))
|
||||
expect(simulationVisible(wrapper)).toBe(false)
|
||||
})
|
||||
|
||||
it("asks core whether the real data has arrived", async ({ expect }) => {
|
||||
|
|
@ -727,7 +704,7 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
expect(wrapper.text()).not.toContain(
|
||||
t("editor.metrics.status.no-data-loaded.title"),
|
||||
)
|
||||
expect(wrapper.text()).toContain(t("editor.metrics.simulation.label"))
|
||||
expect(simulationVisible(wrapper)).toBe(true)
|
||||
expect(
|
||||
wrapper.findComponent({ name: "LineChart" }).props("seriesData"),
|
||||
).toHaveLength(3)
|
||||
|
|
@ -739,7 +716,7 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
wrapper.findComponent({ name: "LineChart" }).props("seriesData"),
|
||||
).toHaveLength(1)
|
||||
}, WAIT_FOR_OPTIONS)
|
||||
expect(wrapper.text()).not.toContain(t("editor.metrics.simulation.label"))
|
||||
expect(simulationVisible(wrapper)).toBe(false)
|
||||
})
|
||||
|
||||
// a block whose queries cannot run is never answered, and holding
|
||||
|
|
@ -764,7 +741,7 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
})
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.text()).not.toContain(t("editor.metrics.simulation.label"))
|
||||
expect(simulationVisible(wrapper)).toBe(false)
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
|
|
@ -826,7 +803,7 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
|
||||
const wrapper = await mountContainer(config, { uid: uid })
|
||||
|
||||
expect(wrapper.text()).toContain(t("editor.metrics.simulation.label"))
|
||||
expect(simulationVisible(wrapper)).toBe(true)
|
||||
|
||||
// core removes the attribute on the live document, which reaches
|
||||
// the editor as a node attribute change
|
||||
|
|
@ -837,7 +814,7 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
await vi.waitFor(() => {
|
||||
expect(calls.length).toBeGreaterThan(0)
|
||||
}, WAIT_FOR_OPTIONS)
|
||||
expect(wrapper.text()).not.toContain(t("editor.metrics.simulation.label"))
|
||||
expect(simulationVisible(wrapper)).toBe(false)
|
||||
})
|
||||
|
||||
it("keeps drawing when the check fails", async ({ expect }) => {
|
||||
|
|
@ -859,3 +836,8 @@ describe("<VisualizationContainer>", { concurrent: false }, () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
// whether the container last reported simulated data as visible
|
||||
function simulationVisible(wrapper: VueWrapper): boolean | undefined {
|
||||
return wrapper.emitted<[boolean]>("simulation-visible")?.at(-1)?.[0]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const props = defineProps<{
|
|||
disableRefresh?: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(event: "loading", v: boolean): void
|
||||
(event: "loading" | "simulation-visible", v: boolean): void
|
||||
(event: "simulate", preset: MetricSimulationPreset): void
|
||||
}>()
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ const fetchMetricData = useMultipleGenericQueries(
|
|||
? debouncedConfig.value.visualizationType
|
||||
: props.config.visualizationType
|
||||
|
||||
if (!queries?.length || !timeRange || !visualizationType) {
|
||||
if (!queries?.length || !timeRange) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +163,14 @@ const canSimulate = computed(() => {
|
|||
return queryRunnable.value && !dataSourceUnavailable.value
|
||||
})
|
||||
|
||||
const simulationOffered = computed(() => {
|
||||
return !isEditingDisabled.value && canSimulate.value
|
||||
})
|
||||
|
||||
const metricsButtonIcon = computed(() => {
|
||||
return isEditingDisabled.value ? "mingcute:eye-line" : "mingcute:pencil-line"
|
||||
})
|
||||
|
||||
// what the block draws, which outlives the attribute by one load
|
||||
const drawnSimulationPreset = computed(() => {
|
||||
return (
|
||||
|
|
@ -310,6 +318,13 @@ watch(
|
|||
},
|
||||
)
|
||||
|
||||
watchImmediate(
|
||||
() => !!drawnSimulationPreset.value,
|
||||
(visible) => {
|
||||
emit("simulation-visible", visible)
|
||||
},
|
||||
)
|
||||
|
||||
// labels series the query gave no legend format for, counting up per
|
||||
// merge call ("Line A", "Line B", ...)
|
||||
function orderedLegendLabel() {
|
||||
|
|
@ -416,13 +431,6 @@ function openModal() {
|
|||
</script>
|
||||
<template>
|
||||
<div class="relative flex size-full min-w-0 items-center justify-center">
|
||||
<div
|
||||
v-if="drawnSimulationPreset"
|
||||
class="pointer-events-none absolute top-2.5 left-1/2 z-1 flex -translate-x-1/2 items-center gap-1 text-2sm text-foreground/70"
|
||||
>
|
||||
<Icon name="lucide:hourglass" class="size-3" />
|
||||
{{ $t("editor.metrics.simulation.label") }}
|
||||
</div>
|
||||
<template v-if="data.status === GenericQueryResultStatus.Ok && data.data">
|
||||
<LineChart
|
||||
v-if="props.config.visualizationType === GenericQueryChartType.Line"
|
||||
|
|
@ -480,8 +488,12 @@ function openModal() {
|
|||
{{ $t("editor.metrics.status.no-data-loaded.description") }}
|
||||
</ShadcnUiEmptyDescription>
|
||||
</ShadcnUiEmptyHeader>
|
||||
<ShadcnUiEmptyContent v-if="!props.hideEmptyActionButton">
|
||||
<ShadcnUiButton variant="outline" size="2sm" @click="openModal">
|
||||
<ShadcnUiEmptyContent
|
||||
v-if="!props.hideEmptyActionButton"
|
||||
:class="simulationOffered && 'grid w-auto grid-cols-2'"
|
||||
>
|
||||
<ShadcnUiButton size="2sm" @click="openModal">
|
||||
<Icon :name="metricsButtonIcon" class="size-3.5" />
|
||||
{{
|
||||
!isEditingDisabled
|
||||
? $t(
|
||||
|
|
@ -493,11 +505,13 @@ function openModal() {
|
|||
}}
|
||||
</ShadcnUiButton>
|
||||
<ShadcnUiButton
|
||||
v-if="!isEditingDisabled && canSimulate"
|
||||
v-if="simulationOffered"
|
||||
variant="outline"
|
||||
size="2sm"
|
||||
class="gap-0.5"
|
||||
@click="startSimulation"
|
||||
>
|
||||
<Icon name="mingcute:test-tube-line" class="size-3.5" />
|
||||
{{
|
||||
$t(
|
||||
"editor.metrics.status.no-data-loaded.simulate-action-button",
|
||||
|
|
@ -521,7 +535,8 @@ function openModal() {
|
|||
</ShadcnUiEmptyDescription>
|
||||
</ShadcnUiEmptyHeader>
|
||||
<ShadcnUiEmptyContent v-if="!props.hideEmptyActionButton">
|
||||
<ShadcnUiButton variant="outline" size="2sm" @click="openModal">
|
||||
<ShadcnUiButton size="2sm" @click="openModal">
|
||||
<Icon :name="metricsButtonIcon" class="size-3.5" />
|
||||
{{
|
||||
!isEditingDisabled
|
||||
? $t(
|
||||
|
|
@ -548,8 +563,14 @@ function openModal() {
|
|||
</ShadcnUiEmptyHeader>
|
||||
<!-- editing the block is what the simplified state is already
|
||||
inside of, but simulating it is not offered anywhere else -->
|
||||
<ShadcnUiEmptyContent v-if="!isEditingDisabled && canSimulate">
|
||||
<ShadcnUiButton variant="outline" size="2sm" @click="startSimulation">
|
||||
<ShadcnUiEmptyContent v-if="simulationOffered">
|
||||
<ShadcnUiButton
|
||||
variant="outline"
|
||||
size="2sm"
|
||||
class="gap-0.5"
|
||||
@click="startSimulation"
|
||||
>
|
||||
<Icon name="mingcute:test-tube-line" class="size-3.5" />
|
||||
{{
|
||||
$t("editor.metrics.status.no-data-loaded.simulate-action-button")
|
||||
}}
|
||||
|
|
@ -557,49 +578,6 @@ function openModal() {
|
|||
</ShadcnUiEmptyContent>
|
||||
</ShadcnUiEmpty>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="data.status === GenericQueryResultStatus.TypeNotSelected"
|
||||
class="text-foreground"
|
||||
>
|
||||
<ShadcnUiEmpty v-if="!props.simplifiedEmpty">
|
||||
<ShadcnUiEmptyHeader>
|
||||
<ShadcnUiEmptyMedia variant="icon" class="size-9">
|
||||
<Icon name="lucide:chart-line" class="size-6" />
|
||||
</ShadcnUiEmptyMedia>
|
||||
<ShadcnUiEmptyTitle>
|
||||
{{ $t("editor.metrics.status.type-not-selected.title") }}
|
||||
</ShadcnUiEmptyTitle>
|
||||
<ShadcnUiEmptyDescription>
|
||||
{{ $t("editor.metrics.status.type-not-selected.description") }}
|
||||
</ShadcnUiEmptyDescription>
|
||||
</ShadcnUiEmptyHeader>
|
||||
<ShadcnUiEmptyContent v-if="!props.hideEmptyActionButton">
|
||||
<ShadcnUiButton variant="outline" size="2sm" @click="openModal">
|
||||
{{
|
||||
!isEditingDisabled
|
||||
? $t(
|
||||
"editor.metrics.status.type-not-selected.normal-action-button",
|
||||
)
|
||||
: $t(
|
||||
"editor.metrics.status.type-not-selected.readonly-action-button",
|
||||
)
|
||||
}}
|
||||
</ShadcnUiButton>
|
||||
</ShadcnUiEmptyContent>
|
||||
</ShadcnUiEmpty>
|
||||
<ShadcnUiEmpty v-else>
|
||||
<ShadcnUiEmptyHeader>
|
||||
<ShadcnUiEmptyMedia variant="icon" class="size-9">
|
||||
<Icon name="lucide:chart-line" class="size-6" />
|
||||
</ShadcnUiEmptyMedia>
|
||||
<ShadcnUiEmptyTitle>
|
||||
{{
|
||||
$t("editor.metrics.status.simplified-config-in-progress.title")
|
||||
}}
|
||||
</ShadcnUiEmptyTitle>
|
||||
</ShadcnUiEmptyHeader>
|
||||
</ShadcnUiEmpty>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
data.status === GenericQueryResultStatus.QueryError &&
|
||||
|
|
@ -638,7 +616,8 @@ function openModal() {
|
|||
</ShadcnUiEmptyDescription>
|
||||
</ShadcnUiEmptyHeader>
|
||||
<ShadcnUiEmptyContent v-if="!props.hideEmptyActionButton">
|
||||
<ShadcnUiButton variant="outline" size="2sm" @click="openModal">
|
||||
<ShadcnUiButton size="2sm" @click="openModal">
|
||||
<Icon :name="metricsButtonIcon" class="size-3.5" />
|
||||
{{
|
||||
!isEditingDisabled
|
||||
? $t("editor.metrics.status.invalid-data.normal-action-button")
|
||||
|
|
@ -664,7 +643,8 @@ function openModal() {
|
|||
</ShadcnUiEmptyDescription>
|
||||
</ShadcnUiEmptyHeader>
|
||||
<ShadcnUiEmptyContent v-if="!props.hideEmptyActionButton">
|
||||
<ShadcnUiButton variant="outline" size="2sm" @click="openModal">
|
||||
<ShadcnUiButton size="2sm" @click="openModal">
|
||||
<Icon :name="metricsButtonIcon" class="size-3.5" />
|
||||
{{
|
||||
!isEditingDisabled
|
||||
? $t("editor.metrics.status.invalid-data.normal-action-button")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { mountSuspended } from "@nuxt/test-utils/runtime"
|
||||
import { enableAutoUnmount } from "@vue/test-utils"
|
||||
import { afterEach, beforeEach, describe, it } from "vitest"
|
||||
import BaseModal from "./BaseModal.client.vue"
|
||||
|
|
@ -20,6 +19,7 @@ import {
|
|||
} from "~/composables/api/test-helpers"
|
||||
import {
|
||||
emitFrom,
|
||||
mountUnderTooltipProvider,
|
||||
stubViewportMatches,
|
||||
t,
|
||||
teleportedButton,
|
||||
|
|
@ -67,7 +67,7 @@ function activateBlock(
|
|||
}
|
||||
|
||||
function mountModal() {
|
||||
return mountSuspended(BaseModal)
|
||||
return mountUnderTooltipProvider(BaseModal, {})
|
||||
}
|
||||
|
||||
function dialogText(): string {
|
||||
|
|
@ -275,6 +275,30 @@ describe("<MetricConfigBaseModal>", { concurrent: false }, () => {
|
|||
).toBe(MetricSimulationPreset.HTTPLatency)
|
||||
})
|
||||
|
||||
it("shows the simulation icon while the preview simulates", async ({
|
||||
expect,
|
||||
}) => {
|
||||
activateBlock()
|
||||
const wrapper = await mountModal()
|
||||
|
||||
emitFrom(wrapper, VisualizationContainer, "simulation-visible", true)
|
||||
await nextTick()
|
||||
|
||||
expect(simulationIconShown()).toBe(true)
|
||||
})
|
||||
|
||||
it("hides the simulation icon while the preview draws real data", async ({
|
||||
expect,
|
||||
}) => {
|
||||
activateBlock()
|
||||
const wrapper = await mountModal()
|
||||
|
||||
emitFrom(wrapper, VisualizationContainer, "simulation-visible", false)
|
||||
await nextTick()
|
||||
|
||||
expect(simulationIconShown()).toBe(false)
|
||||
})
|
||||
|
||||
it("stands aside when the reader asks for the data source settings", async ({
|
||||
expect,
|
||||
}) => {
|
||||
|
|
@ -285,6 +309,15 @@ describe("<MetricConfigBaseModal>", { concurrent: false }, () => {
|
|||
await nextTick()
|
||||
|
||||
expect(useEditorStore().activeMetricBlockConfig).toBeNull()
|
||||
expect(wrapper.emitted("open-settings")).toHaveLength(1)
|
||||
expect(
|
||||
wrapper.findComponent(BaseModal).emitted("open-settings"),
|
||||
).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
// the modal teleports to the body, out of the wrapper's reach
|
||||
function simulationIconShown(): boolean {
|
||||
return (
|
||||
document.body.querySelector('[class~="i-mingcute:test-tube-line"]') !== null
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ const isRefreshIntervalModified = computed(() => {
|
|||
})
|
||||
const chartTopOptionElem = useTemplateRef<HTMLDivElement>("chart-top-options")
|
||||
const { width: chartTopOptionWidth } = useElementSize(chartTopOptionElem)
|
||||
const simulationVisible = ref(false)
|
||||
|
||||
function updateOpenStatus(v: boolean) {
|
||||
if (v) {
|
||||
|
|
@ -147,10 +148,10 @@ function openSettings() {
|
|||
</ShadcnUiDialogHeader>
|
||||
<div
|
||||
v-if="config"
|
||||
class="flex flex-col rounded-md border bg-muted/50 lg:flex-row"
|
||||
class="flex flex-col rounded-md border bg-muted/50 lg:h-126 lg:flex-row"
|
||||
>
|
||||
<div class="flex min-w-0 flex-1 flex-col self-stretch">
|
||||
<div class="relative h-70 min-w-0 border-b">
|
||||
<div class="relative h-70 min-w-0 shrink-0 border-b">
|
||||
<div
|
||||
ref="chart-top-options"
|
||||
:class="
|
||||
|
|
@ -159,6 +160,17 @@ function openSettings() {
|
|||
)
|
||||
"
|
||||
>
|
||||
<ShadcnUiTooltip v-if="simulationVisible">
|
||||
<ShadcnUiTooltipTrigger as-child>
|
||||
<Icon
|
||||
name="mingcute:test-tube-line"
|
||||
class="size-3.5 text-foreground"
|
||||
/>
|
||||
</ShadcnUiTooltipTrigger>
|
||||
<ShadcnUiTooltipContent side="bottom">
|
||||
{{ $t("editor.metrics.simulation.tooltip") }}
|
||||
</ShadcnUiTooltipContent>
|
||||
</ShadcnUiTooltip>
|
||||
<div class="flex flex-col gap-0.5 md:gap-1">
|
||||
<ShadcnUiSelect v-model="config.timeRange">
|
||||
<ShadcnUiSelectTrigger
|
||||
|
|
@ -297,6 +309,7 @@ function openSettings() {
|
|||
simplified-empty
|
||||
:uid="editorStore.activeMetricBlockConfig!"
|
||||
@simulate="(preset) => (config!.simulationPreset = preset)"
|
||||
@simulation-visible="(v) => (simulationVisible = v)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isMinWidth1024px" class="self-stretch border-b">
|
||||
|
|
@ -307,7 +320,7 @@ function openSettings() {
|
|||
@open-settings="openSettings"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="lg:min-h-0 lg:flex-1 lg:overflow-y-auto">
|
||||
<BottomConfig
|
||||
:model-value="config"
|
||||
:diff-status="diffStatus"
|
||||
|
|
@ -315,7 +328,10 @@ function openSettings() {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isMinWidth1024px" class="w-70 self-stretch border-l">
|
||||
<div
|
||||
v-if="isMinWidth1024px"
|
||||
class="w-70 self-stretch border-l lg:overflow-y-auto"
|
||||
>
|
||||
<ConfigSidebar
|
||||
:model-value="config"
|
||||
:diff-status="diffStatus"
|
||||
|
|
|
|||
|
|
@ -287,6 +287,34 @@ function removeThreshold(index: number) {
|
|||
</div>
|
||||
<div class="mt-1 h-px bg-border" />
|
||||
</div>
|
||||
<div v-if="config.simulationPreset">
|
||||
<div class="flex flex-col gap-1 px-1.75">
|
||||
<span class="text-2sm font-medium">
|
||||
{{ $t("editor.metrics.simulation.section-title") }}
|
||||
</span>
|
||||
<span class="mb-1 text-2xs text-muted-foreground">
|
||||
{{ $t("editor.metrics.simulation.section-description") }}
|
||||
</span>
|
||||
<ConfigField>
|
||||
<template #label>
|
||||
<span>
|
||||
{{ $t("editor.metrics.simulation.preset-label") }}
|
||||
</span>
|
||||
</template>
|
||||
<SimulationPresetSelect v-model="config.simulationPreset" />
|
||||
</ConfigField>
|
||||
<ShadcnUiButton
|
||||
v-if="!isEditingDisabled"
|
||||
variant="outline-transparent"
|
||||
size="custom"
|
||||
class="h-[1.775rem]! justify-center px-1.5 text-2sm font-normal"
|
||||
@click="config.simulationPreset = null"
|
||||
>
|
||||
{{ $t("editor.metrics.simulation.stop-button") }}
|
||||
</ShadcnUiButton>
|
||||
</div>
|
||||
<div class="mt-2 h-px bg-border" />
|
||||
</div>
|
||||
<div class="px-1.75">
|
||||
<ConfigField>
|
||||
<template #label>
|
||||
|
|
@ -595,33 +623,5 @@ function removeThreshold(index: number) {
|
|||
/>
|
||||
</ConfigField>
|
||||
</div>
|
||||
<div v-if="config.simulationPreset">
|
||||
<div class="mb-2 h-px bg-border" />
|
||||
<div class="flex flex-col gap-1 px-1.75">
|
||||
<span class="text-2sm font-medium">
|
||||
{{ $t("editor.metrics.simulation.section-title") }}
|
||||
</span>
|
||||
<span class="text-2xs text-muted-foreground">
|
||||
{{ $t("editor.metrics.simulation.section-description") }}
|
||||
</span>
|
||||
<ConfigField>
|
||||
<template #label>
|
||||
<span>
|
||||
{{ $t("editor.metrics.simulation.preset-label") }}
|
||||
</span>
|
||||
</template>
|
||||
<SimulationPresetSelect v-model="config.simulationPreset" />
|
||||
</ConfigField>
|
||||
<ShadcnUiButton
|
||||
v-if="!isEditingDisabled"
|
||||
variant="outline-transparent"
|
||||
size="custom"
|
||||
class="h-[1.775rem]! justify-center px-1.5 text-2sm font-normal"
|
||||
@click="config.simulationPreset = null"
|
||||
>
|
||||
{{ $t("editor.metrics.simulation.stop-button") }}
|
||||
</ShadcnUiButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ describe("<VisualizationTypeSelect>", { concurrent: false }, () => {
|
|||
})
|
||||
|
||||
it("offers the three visualization types", async ({ expect }) => {
|
||||
const wrapper = await mountTypeSelect({ modelValue: configWith(null) })
|
||||
const wrapper = await mountTypeSelect({
|
||||
modelValue: configWith(GenericQueryChartType.Line),
|
||||
})
|
||||
|
||||
expect(tiles(wrapper).map((t) => t.text())).toEqual([
|
||||
t("editor.metrics.config.type-options.line-chart.title"),
|
||||
|
|
@ -52,22 +54,26 @@ describe("<VisualizationTypeSelect>", { concurrent: false }, () => {
|
|||
expect(at(tiles(wrapper), LINE).classes()).not.toContain("border-primary")
|
||||
})
|
||||
|
||||
it("marks nothing while no type is chosen", async ({ expect }) => {
|
||||
const wrapper = await mountTypeSelect({ modelValue: configWith(null) })
|
||||
|
||||
expect(
|
||||
tiles(wrapper).filter((t) => t.classes().includes("border-primary")),
|
||||
).toHaveLength(0)
|
||||
})
|
||||
|
||||
it.for([
|
||||
{ index: LINE, expected: GenericQueryChartType.Line },
|
||||
{ index: BAR, expected: GenericQueryChartType.Bar },
|
||||
{ index: GAUGE, expected: GenericQueryChartType.Gauge },
|
||||
{
|
||||
index: LINE,
|
||||
input: GenericQueryChartType.Gauge,
|
||||
expected: GenericQueryChartType.Line,
|
||||
},
|
||||
{
|
||||
index: BAR,
|
||||
input: GenericQueryChartType.Line,
|
||||
expected: GenericQueryChartType.Bar,
|
||||
},
|
||||
{
|
||||
index: GAUGE,
|
||||
input: GenericQueryChartType.Line,
|
||||
expected: GenericQueryChartType.Gauge,
|
||||
},
|
||||
])(
|
||||
"stores the $expected type when picked",
|
||||
async ({ index, expected }, { expect }) => {
|
||||
const config = configWith(null)
|
||||
async ({ index, input, expected }, { expect }) => {
|
||||
const config = configWith(input)
|
||||
const wrapper = await mountTypeSelect({ modelValue: config })
|
||||
|
||||
await at(tiles(wrapper), index).trigger("click")
|
||||
|
|
@ -78,22 +84,22 @@ describe("<VisualizationTypeSelect>", { concurrent: false }, () => {
|
|||
|
||||
it("changes nothing in read mode", async ({ expect }) => {
|
||||
useEditorMeta().setEditable(false)
|
||||
const config = configWith(null)
|
||||
const config = configWith(GenericQueryChartType.Line)
|
||||
const wrapper = await mountTypeSelect({ modelValue: config })
|
||||
|
||||
await at(tiles(wrapper), BAR).trigger("click")
|
||||
|
||||
expect(config.visualizationType).toBeNull()
|
||||
expect(config.visualizationType).toBe(GenericQueryChartType.Line)
|
||||
})
|
||||
|
||||
it("changes nothing while a reviewable diff is shown", async ({ expect }) => {
|
||||
useEditorStore().setReviewableDiffActive(true)
|
||||
const config = configWith(null)
|
||||
const config = configWith(GenericQueryChartType.Line)
|
||||
const wrapper = await mountTypeSelect({ modelValue: config })
|
||||
|
||||
await at(tiles(wrapper), BAR).trigger("click")
|
||||
|
||||
expect(config.visualizationType).toBeNull()
|
||||
expect(config.visualizationType).toBe(GenericQueryChartType.Line)
|
||||
})
|
||||
|
||||
it("changes nothing when there is no config to edit", async ({ expect }) => {
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ describe("MetricBlock", () => {
|
|||
config: null,
|
||||
title: "",
|
||||
dataSourceId: null,
|
||||
visualizationType: null,
|
||||
visualizationType: GenericQueryChartType.Line,
|
||||
queries: [{ name: "Query 1", query: "", legendFormat: "" }],
|
||||
timeRange: TimeRangePreset.Last5Minutes,
|
||||
refreshInterval: RefreshInterval.M5,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ describe("defaultMetricConfig", () => {
|
|||
expect(defaultMetricConfig()).toEqual({
|
||||
title: "",
|
||||
dataSourceId: null,
|
||||
visualizationType: null,
|
||||
visualizationType: GenericQueryChartType.Line,
|
||||
queries: [{ name: "Query 1", query: "", legendFormat: "" }],
|
||||
timeRange: TimeRangePreset.Last5Minutes,
|
||||
refreshInterval: RefreshInterval.M5,
|
||||
|
|
@ -103,7 +103,7 @@ describe("buildConfigFromNodeAttrs", () => {
|
|||
expect(buildConfigFromNodeAttrs({ config: {} })).toEqual({
|
||||
title: "",
|
||||
dataSourceId: null,
|
||||
visualizationType: null,
|
||||
visualizationType: GenericQueryChartType.Line,
|
||||
queries: null,
|
||||
timeRange: null,
|
||||
refreshInterval: null,
|
||||
|
|
@ -161,7 +161,7 @@ describe("buildConfigFromNodeAttrs", () => {
|
|||
expect(buildConfigFromNodeAttrs({})).toEqual({
|
||||
title: "",
|
||||
dataSourceId: null,
|
||||
visualizationType: null,
|
||||
visualizationType: GenericQueryChartType.Line,
|
||||
queries: null,
|
||||
timeRange: null,
|
||||
refreshInterval: null,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export enum MetricSimulationPreset {
|
|||
export interface MetricConfig {
|
||||
title: string
|
||||
dataSourceId: string | null
|
||||
visualizationType: GenericQueryChartType | null
|
||||
visualizationType: GenericQueryChartType
|
||||
queries:
|
||||
| {
|
||||
name: string
|
||||
|
|
@ -118,7 +118,7 @@ export function defaultMetricConfig(): MetricConfig {
|
|||
return {
|
||||
title: "",
|
||||
dataSourceId: null,
|
||||
visualizationType: null,
|
||||
visualizationType: GenericQueryChartType.Line,
|
||||
// NOTE: this doesn't use i18n because this isn't a reactive context
|
||||
queries: [{ name: "Query 1", query: "", legendFormat: "" }],
|
||||
timeRange: TimeRangePreset.Last5Minutes,
|
||||
|
|
@ -153,7 +153,8 @@ export function buildConfigFromNodeAttrs(
|
|||
return {
|
||||
title: legacy.title ?? "",
|
||||
dataSourceId: legacy.dataSourceId ?? null,
|
||||
visualizationType: legacy.type ?? legacy.visualizationType ?? null,
|
||||
visualizationType:
|
||||
legacy.type ?? legacy.visualizationType ?? GenericQueryChartType.Line,
|
||||
queries: legacy.queries ?? null,
|
||||
timeRange: legacy.timeRange ?? null,
|
||||
refreshInterval: legacy.refreshInterval ?? null,
|
||||
|
|
@ -178,7 +179,7 @@ export function buildConfigFromNodeAttrs(
|
|||
dataSourceId: (attrs.dataSourceId as string | null | undefined) ?? null,
|
||||
visualizationType:
|
||||
(attrs.visualizationType as GenericQueryChartType | null | undefined) ??
|
||||
null,
|
||||
GenericQueryChartType.Line,
|
||||
queries: (attrs.queries as MetricConfig["queries"] | undefined) ?? null,
|
||||
timeRange: (attrs.timeRange as TimeRangePreset | null | undefined) ?? null,
|
||||
refreshInterval:
|
||||
|
|
|
|||
|
|
@ -174,23 +174,6 @@ describe("indexToAlphabeticLabel", () => {
|
|||
})
|
||||
|
||||
describe("mergeVisualizationResults", () => {
|
||||
it.for([
|
||||
{ name: "null", type: null },
|
||||
{ name: "undefined", type: undefined },
|
||||
])(
|
||||
"reports type-not-selected when the chart type is $name",
|
||||
({ type }, { expect }) => {
|
||||
expect(
|
||||
mergeVisualizationResults(
|
||||
type,
|
||||
[queryItem(okResult([]))],
|
||||
null,
|
||||
orderedLabel,
|
||||
),
|
||||
).toEqual({ status: GenericQueryResultStatus.TypeNotSelected })
|
||||
},
|
||||
)
|
||||
|
||||
it.for([
|
||||
{ name: "null", results: null },
|
||||
{ name: "undefined", results: undefined },
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ function truncToDecimals(num: number, decimals: number | null) {
|
|||
}
|
||||
|
||||
function extractVisualizationData(
|
||||
visualizationType: GenericQueryChartType | null | undefined,
|
||||
visualizationType: GenericQueryChartType,
|
||||
input: GenericQueryResult | null | undefined,
|
||||
legendLabelFormat: string,
|
||||
decimals: number | null,
|
||||
|
|
@ -112,10 +112,6 @@ function extractVisualizationData(
|
|||
queryErrorMessage?: string
|
||||
data?: MultipleLineChartData | MultipleBarChartData | MultipleGaugeChartData
|
||||
} {
|
||||
if (!visualizationType) {
|
||||
return { status: GenericQueryResultStatus.TypeNotSelected }
|
||||
}
|
||||
|
||||
if (!input) {
|
||||
return { status: GenericQueryResultStatus.NoData }
|
||||
}
|
||||
|
|
@ -227,7 +223,7 @@ export interface QueryResultItem {
|
|||
}
|
||||
|
||||
export function mergeVisualizationResults(
|
||||
visualizationType: GenericQueryChartType | null | undefined,
|
||||
visualizationType: GenericQueryChartType,
|
||||
queryResults: QueryResultItem[] | null | undefined,
|
||||
decimals: number | null,
|
||||
orderedLegendLabelFn: (type: GenericQueryChartType) => string,
|
||||
|
|
@ -236,10 +232,6 @@ export function mergeVisualizationResults(
|
|||
queryErrorMessage?: string
|
||||
data?: MultipleLineChartData | MultipleBarChartData | MultipleGaugeChartData
|
||||
} {
|
||||
if (!visualizationType) {
|
||||
return { status: GenericQueryResultStatus.TypeNotSelected }
|
||||
}
|
||||
|
||||
if (!queryResults?.length) {
|
||||
return { status: GenericQueryResultStatus.NoData }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ function metricConfig(title: string): MetricConfig {
|
|||
return {
|
||||
title,
|
||||
dataSourceId: null,
|
||||
visualizationType: null,
|
||||
visualizationType: GenericQueryChartType.Line,
|
||||
queries: null,
|
||||
timeRange: null,
|
||||
refreshInterval: null,
|
||||
|
|
|
|||
|
|
@ -437,12 +437,6 @@
|
|||
"simplified-config-in-progress": {
|
||||
"title": "No Data"
|
||||
},
|
||||
"type-not-selected": {
|
||||
"title": "No Visualization Selected",
|
||||
"description": "Start configuring metrics by selecting a visualization type",
|
||||
"normal-action-button": "Edit Metrics",
|
||||
"readonly-action-button": "View Metrics"
|
||||
},
|
||||
"data-source-not-selected": {
|
||||
"title": "No Data Source Selected",
|
||||
"description": "Choose a data source to see metric data",
|
||||
|
|
@ -464,7 +458,7 @@
|
|||
"description": "The current configuration returned no data. Please adjust your queries or data sources",
|
||||
"normal-action-button": "Edit Metrics",
|
||||
"readonly-action-button": "View Metrics",
|
||||
"simulate-action-button": "Simulate data"
|
||||
"simulate-action-button": "Simulate Data"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
|
|
@ -783,12 +777,12 @@
|
|||
}
|
||||
},
|
||||
"simulation": {
|
||||
"label": "Simulation",
|
||||
"tooltip": "Showing simulated data until real data is available",
|
||||
"section-title": "Simulated data",
|
||||
"section-description": "Simulate the shape this chart will take until the metric it documents becomes available.",
|
||||
"preset-label": "Chart",
|
||||
"preset-placeholder": "Select a chart",
|
||||
"stop-button": "Stop simulating",
|
||||
"section-description": "The chart shows sample data in the pattern below until real data is available.",
|
||||
"preset-label": "Data pattern",
|
||||
"preset-placeholder": "Select a pattern",
|
||||
"stop-button": "Remove Simulated Data",
|
||||
"preset-options": {
|
||||
"cpu_usage": "CPU usage",
|
||||
"memory_usage": "Memory usage",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user