diff --git a/package-lock.json b/package-lock.json index 78d4b9eba1a..fc7d30bd968 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19785,6 +19785,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-void-elements": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", @@ -36113,6 +36120,7 @@ "fflate": "^0.8.2", "globals": "^16.0.0", "gridstack": "^11.3.0", + "html-to-image": "^1.11.13", "jsdom": "^26.0.0", "json-schema": "^0.4.0", "lucide-svelte": "^0.298.0", diff --git a/web-common/package.json b/web-common/package.json index cb7343d0923..fbc8a1d5ece 100644 --- a/web-common/package.json +++ b/web-common/package.json @@ -73,6 +73,7 @@ "fflate": "^0.8.2", "globals": "^16.0.0", "gridstack": "^11.3.0", + "html-to-image": "^1.11.13", "jsdom": "^26.0.0", "json-schema": "^0.4.0", "lucide-svelte": "^0.298.0", diff --git a/web-common/src/features/dashboards/big-number/MeasureBigNumber.svelte b/web-common/src/features/dashboards/big-number/MeasureBigNumber.svelte index 346be5eab73..5ca499c9f7e 100644 --- a/web-common/src/features/dashboards/big-number/MeasureBigNumber.svelte +++ b/web-common/src/features/dashboards/big-number/MeasureBigNumber.svelte @@ -40,6 +40,7 @@ export let comparisonTimeEnd: string | undefined = undefined; export let showComparison = false; export let ready: boolean = true; + export let skipLink: boolean = false; const client = useRuntimeClient(); @@ -189,7 +190,7 @@ isMeasureExpanded = true; } }; - $: useDiv = isMeasureExpanded || !withTimeseries; + $: useDiv = isMeasureExpanded || !withTimeseries || skipLink; function handleMouseOver() { cellInspectorStore.updateValue(value, tooltipValue); @@ -300,11 +301,10 @@ copyValue = measureValueFormatterUnabridged(value) ?? "no data"; }} - class="w-fit {( - lowerIsBetter ? isComparisonNegative : isComparisonPositive - ) - ? 'font-semibold' - : ''} {comparisonDeltaColorClass}" + class="w-fit {comparisonDeltaColorClass}" + class:font-semibold={lowerIsBetter + ? isComparisonNegative + : isComparisonPositive} > @@ -392,39 +402,54 @@ /> {#if activeTimeGrain} - handlePan("left")} - onPanRight={() => handlePan("right")} - {showComparison} - {showTimeDimensionDetail} - dynamicYAxis={dynamicYAxisScale} - onScrub={handleScrub} - onScrubClear={() => { - metricsExplorerStore.setSelectedScrubRange( - exploreName, - undefined, - ); - }} - /> +
+ handlePan("left")} + onPanRight={() => handlePan("right")} + {showComparison} + {showTimeDimensionDetail} + dynamicYAxis={dynamicYAxisScale} + onScrub={handleScrub} + onScrubClear={() => { + metricsExplorerStore.setSelectedScrubRange( + exploreName, + undefined, + ); + }} + /> + + + + + + + openScreenshotDialog(measure)} + > + Download as PNG + + + +
{:else}
@@ -442,3 +467,23 @@ }} onReplace={createPivot} /> + +{#if screenshotDialogMeasure} + +{/if} diff --git a/web-common/src/features/dashboards/time-series/ScreenshotContainer.svelte b/web-common/src/features/dashboards/time-series/ScreenshotContainer.svelte new file mode 100644 index 00000000000..ffd3bc587ab --- /dev/null +++ b/web-common/src/features/dashboards/time-series/ScreenshotContainer.svelte @@ -0,0 +1,178 @@ + + + + + + Export chart + + + +
+
+
+

+ {measure.displayName || measure.name} +

+ {#if measure.description} +

{measure.description}

+ {/if} +
+
+
{formattedTimeRange}
+
+ + + +
+ {#if timeGranularity} +
+
+ +
+ {/if} + + + + {#if timeGranularity} + + {/if} +
+ +
+ Rill + Generated {generatedTime} +
+
+
+ + + + + +
+
diff --git a/web-common/src/features/dashboards/time-series/measure-chart/MeasureChart.svelte b/web-common/src/features/dashboards/time-series/measure-chart/MeasureChart.svelte index 42481ba7560..f630666f2f8 100644 --- a/web-common/src/features/dashboards/time-series/measure-chart/MeasureChart.svelte +++ b/web-common/src/features/dashboards/time-series/measure-chart/MeasureChart.svelte @@ -61,7 +61,7 @@ export let onScrubClear: (() => void) | undefined = undefined; export let onPanLeft: (() => void) | undefined = undefined; export let onPanRight: (() => void) | undefined = undefined; - export let scrubController: ScrubController; + export let scrubController: ScrubController | undefined = undefined; export let connectNulls: boolean = true; export let dynamicYAxis: boolean = false; diff --git a/web-common/src/features/dashboards/time-series/measure-chart/MeasureChartBody.svelte b/web-common/src/features/dashboards/time-series/measure-chart/MeasureChartBody.svelte index 9c5630ca89c..3f46d4a52ad 100644 --- a/web-common/src/features/dashboards/time-series/measure-chart/MeasureChartBody.svelte +++ b/web-common/src/features/dashboards/time-series/measure-chart/MeasureChartBody.svelte @@ -76,7 +76,7 @@ }) => void) | undefined; export let onScrubClear: (() => void) | undefined; - export let scrubController: ScrubController; + export let scrubController: ScrubController | undefined = undefined; export let metricsViewName: string; export let connectNulls: boolean = true; export let dynamicYAxis: boolean = false; @@ -141,12 +141,12 @@ $: xTickIndices = computeXTickIndices(mode, data.length); // Keep scrub controller in sync with data length - $: scrubController.setDataLength(data.length); + $: scrubController?.setDataLength(data.length); // Subscribe to scrub state from controller for rendering - $: scrubStateStore = scrubController.state; + $: scrubStateStore = scrubController?.state; $: currentScrubState = $scrubStateStore; - $: isScrubbing = currentScrubState.isScrubbing; + $: isScrubbing = Boolean(currentScrubState?.isScrubbing); // Scrub indices: use local (active) state while scrubbing, external (URL) state otherwise $: externalScrubStartIndex = chartScrubInterval @@ -155,8 +155,8 @@ $: externalScrubEndIndex = chartScrubInterval ? dateToIndex(data, chartScrubInterval.end.toMillis()) : null; - $: scrubStartIndex = currentScrubState.startIndex ?? externalScrubStartIndex; - $: scrubEndIndex = currentScrubState.endIndex ?? externalScrubEndIndex; + $: scrubStartIndex = currentScrubState?.startIndex ?? externalScrubStartIndex; + $: scrubEndIndex = currentScrubState?.endIndex ?? externalScrubEndIndex; $: hasScrubSelection = scrubStartIndex !== null && scrubEndIndex !== null; // Hover state @@ -178,7 +178,7 @@ $: hoveredIndex = $hoverIndex?.start ?? -1; $: hoveredPoint = data[hoveredIndex] ?? null; - $: cursorStyle = scrubController.getCursorStyle(hoverState.screenX, xScale); + $: cursorStyle = scrubController?.getCursorStyle(hoverState.screenX, xScale); // Formatters $: measureFormatter = createMeasureValueFormatter(measure); @@ -264,7 +264,7 @@ function handleReset() { onScrubClear?.(); - scrubController.reset(); + scrubController?.reset(); } function handleSvgMouseLeave() { @@ -275,7 +275,7 @@ } function handleMouseDown(e: MouseEvent) { - if (e.button !== 0) return; + if (!scrubController || e.button !== 0) return; mouseDownX = e.clientX; mouseDownY = e.clientY; const x = clampX(e.offsetX); @@ -308,7 +308,7 @@ const [start, end] = s < e ? [s, e] : [e, s]; measureSelection.setRange(measureName, start, end); } - scrubController.reset(); + scrubController?.reset(); } function handlePointClick(offsetX: number) { @@ -324,6 +324,8 @@ } function handleMouseUp(e: MouseEvent) { + if (!scrubController) return; + const wasClick = mouseDownX !== null && mouseDownY !== null && @@ -383,7 +385,7 @@ if (isOutside) { // Click outside selection clears it - scrubController.reset(); + scrubController?.reset(); onScrubClear?.(); measureSelection.clear(); } else if (measureName) { @@ -421,7 +423,7 @@ }; // Update scrub if dragging - if (get(scrubController.state).isScrubbing) { + if (scrubController && get(scrubController.state).isScrubbing) { scrubController.update(x, xScale); } diff --git a/web-common/src/features/themes/selectors.ts b/web-common/src/features/themes/selectors.ts index d05ab43a6a2..5a55017bddc 100644 --- a/web-common/src/features/themes/selectors.ts +++ b/web-common/src/features/themes/selectors.ts @@ -72,6 +72,7 @@ export function createResolvedThemeStore( queryClient, ); return themeQuery.subscribe(($themeQuery) => { + console.log(name, $themeQuery.data); if ($themeQuery.data?.theme?.spec) { set(new Theme($themeQuery.data.theme.spec)); } else {