diff --git a/proto/rill/ui/v1/dashboard.proto b/proto/rill/ui/v1/dashboard.proto index b33634f954e..b86ad5f780f 100644 --- a/proto/rill/ui/v1/dashboard.proto +++ b/proto/rill/ui/v1/dashboard.proto @@ -89,6 +89,8 @@ message DashboardState { // Expanded measure for TDD view optional string expanded_measure = 18; + + optional int32 pin_index = 19; } message DashboardTimeRange { diff --git a/web-common/src/features/dashboards/actions/index.ts b/web-common/src/features/dashboards/actions/index.ts index 88f2e71c72a..8957e315bff 100644 --- a/web-common/src/features/dashboards/actions/index.ts +++ b/web-common/src/features/dashboards/actions/index.ts @@ -22,6 +22,7 @@ export function clearFilterForDimension( (dimensionValues) => dimensionValues.name === dimensionId ); } + dashboard.pinIndex = -1; }); } @@ -36,6 +37,7 @@ export function clearAllFilters(ctx: StateManagers) { dashboard.filters.include = []; dashboard.filters.exclude = []; dashboard.dimensionFilterExcludeMode.clear(); + dashboard.pinIndex = -1; }); } } @@ -60,18 +62,27 @@ export function toggleDimensionValue( ); if (dimensionEntryIndex >= 0) { - if ( - removeIfExists( - dashboard.filters[relevantFilterKey][dimensionEntryIndex].in, - (value) => value === dimensionValue - ) - ) { + const index = dashboard.filters[relevantFilterKey][ + dimensionEntryIndex + ].in?.findIndex((value) => value === dimensionValue) as number; + + if (index >= 0) { + dashboard.filters[relevantFilterKey][dimensionEntryIndex].in?.splice( + index, + 1 + ); if ( dashboard.filters[relevantFilterKey][dimensionEntryIndex].in .length === 0 ) { dashboard.filters[relevantFilterKey].splice(dimensionEntryIndex, 1); } + + // Only decrement pinIndex if the removed value was before the pinned value + if (dashboard.pinIndex >= index) { + dashboard.pinIndex--; + } + return; } diff --git a/web-common/src/features/dashboards/proto-state/fromProto.ts b/web-common/src/features/dashboards/proto-state/fromProto.ts index 5eef2878591..f1a82f270b7 100644 --- a/web-common/src/features/dashboards/proto-state/fromProto.ts +++ b/web-common/src/features/dashboards/proto-state/fromProto.ts @@ -101,7 +101,9 @@ export function getDashboardStateFromProto( } else { entity.selectedComparisonDimension = undefined; } - + if (dashboard.pinIndex !== undefined) { + entity.pinIndex = dashboard.pinIndex; + } if (dashboard.selectedTimezone) { entity.selectedTimezone = dashboard.selectedTimezone; } diff --git a/web-common/src/features/dashboards/proto-state/toProto.ts b/web-common/src/features/dashboards/proto-state/toProto.ts index 909b27abcb3..229cfd9f385 100644 --- a/web-common/src/features/dashboards/proto-state/toProto.ts +++ b/web-common/src/features/dashboards/proto-state/toProto.ts @@ -82,6 +82,9 @@ export function getProtoFromDashboardState( if (metrics.expandedMeasureName) { state.expandedMeasure = metrics.expandedMeasureName; } + if (metrics.pinIndex !== undefined) { + state.pinIndex = metrics.pinIndex; + } if (metrics.selectedDimensionName) { state.selectedDimension = metrics.selectedDimensionName; } diff --git a/web-common/src/features/dashboards/stores/dashboard-store-defaults.ts b/web-common/src/features/dashboards/stores/dashboard-store-defaults.ts index 5fc941f60f8..8dbdcbb251c 100644 --- a/web-common/src/features/dashboards/stores/dashboard-store-defaults.ts +++ b/web-common/src/features/dashboards/stores/dashboard-store-defaults.ts @@ -131,6 +131,7 @@ export function getDefaultMetricsExplorerEntity( showTimeComparison: false, dimensionSearchText: "", + pinIndex: -1, }; // set time range related stuff setDefaultTimeRange(metricsView, metricsExplorer, fullTimeRange); diff --git a/web-common/src/features/dashboards/stores/dashboard-stores.ts b/web-common/src/features/dashboards/stores/dashboard-stores.ts index 6471b8cd4ca..a55c369bb56 100644 --- a/web-common/src/features/dashboards/stores/dashboard-stores.ts +++ b/web-common/src/features/dashboards/stores/dashboard-stores.ts @@ -222,6 +222,21 @@ const metricViewReducers = { setExpandedMeasureName(name: string, measureName: string) { updateMetricsExplorerByName(name, (metricsExplorer) => { metricsExplorer.expandedMeasureName = measureName; + + // If going into TDD view and already having a comparison dimension, + // then set the pinIndex + if (metricsExplorer.selectedComparisonDimension) { + metricsExplorer.pinIndex = getPinIndexForDimension( + metricsExplorer, + metricsExplorer.selectedComparisonDimension + ); + } + }); + }, + + setPinIndex(name: string, index: number) { + updateMetricsExplorerByName(name, (metricsExplorer) => { + metricsExplorer.pinIndex = index; }); }, @@ -293,6 +308,10 @@ const metricViewReducers = { setDisplayComparison(metricsExplorer, false); } metricsExplorer.selectedComparisonDimension = dimensionName; + metricsExplorer.pinIndex = getPinIndexForDimension( + metricsExplorer, + dimensionName + ); }); }, @@ -492,12 +511,21 @@ const metricViewReducers = { if (dimensionEntryIndex >= 0) { const filtersIn = filters[dimensionEntryIndex].in; - // if (filtersIn === undefined) return; - if (removeIfExists(filtersIn, (value) => value === dimensionValue)) { + + const index = filtersIn?.findIndex( + (value) => value === dimensionValue + ) as number; + if (index >= 0) { + filtersIn?.splice(index, 1); if (filtersIn.length === 0) { filters.splice(dimensionEntryIndex, 1); } + + // Only decrement pinIndex if the removed value was before the pinned value + if (metricsExplorer.pinIndex >= index) { + metricsExplorer.pinIndex--; + } return; } filtersIn.push(dimensionValue); @@ -515,6 +543,7 @@ const metricViewReducers = { metricsExplorer.filters.include = []; metricsExplorer.filters.exclude = []; metricsExplorer.dimensionFilterExcludeMode.clear(); + metricsExplorer.pinIndex = -1; }); }, @@ -653,3 +682,27 @@ function setSelectedScrubRange( metricsExplorer.selectedScrubRange = scrubRange; } + +function getPinIndexForDimension( + metricsExplorer: MetricsExplorerEntity, + dimensionName: string +) { + const relevantFilterKey = metricsExplorer.dimensionFilterExcludeMode.get( + dimensionName + ) + ? "exclude" + : "include"; + + const dimensionEntryIndex = metricsExplorer.filters[ + relevantFilterKey + ].findIndex((filter) => filter.name === dimensionName); + + if (dimensionEntryIndex >= 0) { + return ( + metricsExplorer.filters[relevantFilterKey][dimensionEntryIndex]?.in + ?.length - 1 + ); + } + + return -1; +} diff --git a/web-common/src/features/dashboards/stores/metrics-explorer-entity.ts b/web-common/src/features/dashboards/stores/metrics-explorer-entity.ts index 5e36f46c8c1..db82cca3d63 100644 --- a/web-common/src/features/dashboards/stores/metrics-explorer-entity.ts +++ b/web-common/src/features/dashboards/stores/metrics-explorer-entity.ts @@ -62,6 +62,13 @@ export interface MetricsExplorerEntity { */ expandedMeasureName?: string; + /** + * The index at which selected dimension values are pinned in the + * time detailed dimension view. Values above this index preserve + * their original order + */ + pinIndex: number; + /** * This is the sort type that will be used for the leaderboard * and dimension detail table. See SortType for more details. diff --git a/web-common/src/features/dashboards/time-dimension-details/TDDIcons.ts b/web-common/src/features/dashboards/time-dimension-details/TDDIcons.ts index 21ce8d3e59e..7ce30294ddf 100644 --- a/web-common/src/features/dashboards/time-dimension-details/TDDIcons.ts +++ b/web-common/src/features/dashboards/time-dimension-details/TDDIcons.ts @@ -58,3 +58,30 @@ export const ExcludeIcon = ` `; + +const PinIcon = ( + fill +) => ``; + +export const PinSetIcon = PinIcon("#374151"); +export const PinSetHoverIcon = PinIcon("#9CA3AF"); + +export const PinUnsetIcon = ``; + +export const PinHoverUnsetIcon = ``; diff --git a/web-common/src/features/dashboards/time-dimension-details/TDDTable.svelte b/web-common/src/features/dashboards/time-dimension-details/TDDTable.svelte index 695599c6406..9ac60d937eb 100644 --- a/web-common/src/features/dashboards/time-dimension-details/TDDTable.svelte +++ b/web-common/src/features/dashboards/time-dimension-details/TDDTable.svelte @@ -11,6 +11,10 @@ ExcludeIcon, MeasureArrow, PieChart, + PinSetIcon, + PinSetHoverIcon, + PinHoverUnsetIcon, + PinUnsetIcon, } from "@rilldata/web-common/features/dashboards/time-dimension-details/TDDIcons"; import type { TableData, TablePosition, TDDComparison } from "./types"; import { SortType } from "@rilldata/web-common/features/dashboards/proto-state/derived-types"; @@ -24,6 +28,7 @@ export let sortType: SortType; export let highlightedCol: number; export let scrubPos: { start: number; end: number }; + export let pinIndex: number; export let comparing: TDDComparison; export let tableData: TableData; @@ -36,6 +41,7 @@ let rowIdxHover: number | undefined; let colIdxHover: number | undefined; + let hoveringPin = false; function getBodyData(pos: PivotPos) { return tableData?.body @@ -54,6 +60,8 @@ const renderCell: PivotRenderCallback = (data) => { const classesToAdd = ["text-right"]; const classesToRemove = [ + "border-b", + "border-gray-200", "bg-white", "bg-gray-100", "bg-gray-200", @@ -65,12 +73,12 @@ "bg-slate-200", ]; - if (data.y === 2) { - if (comparing === "time") { - classesToAdd.push("border-b", "border-gray-200"); - } else { - classesToRemove.push("border-b", "border-gray-200"); - } + if (pinIndex > -1 && comparing === "dimension" && data.y === pinIndex + 1) { + classesToAdd.push("border-b", "border-gray-200"); + } + + if (comparing === "time" && data.y === 2) { + classesToAdd.push("border-b", "border-gray-200"); } const isScrubbed = @@ -104,14 +112,6 @@ return timeFormatter(data.value.value); }; - // Visible line list - const toggleVisible = (n) => { - n = parseInt(n); - if (comparing != "dimension" || n == 0) return; - const label = tableData?.rowHeaderData[n][0].value; - dispatch("toggle-filter", label); - }; - // Any time visible line list changes, redraw the table $: { scrubPos; @@ -120,11 +120,22 @@ pivot?.draw(); } + const getPinIcon = () => { + if (comparing === "dimension") { + if (tableData?.selectedValues.length === 0) return ""; + else if (pinIndex === tableData?.selectedValues.length - 1) + return hoveringPin ? PinHoverUnsetIcon : PinSetIcon; + else return hoveringPin ? PinSetHoverIcon : PinUnsetIcon; + } else { + return ""; + } + }; + let noSelectionMarkerCount = 0; const getMarker = (value, y) => { if (y === 0) { noSelectionMarkerCount = 0; - return { icon: ``, muted: false }; + return { icon: "", muted: false }; } const visibleIdx = tableData?.selectedValues.indexOf(value.value); @@ -165,12 +176,13 @@ }; const renderRowHeader: PivotRenderCallback = ({ value, x, y, element }) => { - if (y === 2) { - if (comparing === "time") { - element.classList.add("border-b", "border-gray-200"); - } else { - element.classList.remove("border-b", "border-gray-200"); - } + const showBorder = + (pinIndex > -1 && comparing === "dimension" && y === pinIndex + 1) || + (comparing === "time" && y === 2); + if (showBorder) { + element.classList.add("border-b", "border-gray-200"); + } else { + element.classList.remove("border-b", "border-gray-200"); } const cellBgColor = getClassForCell( @@ -195,15 +207,14 @@ element?.parentElement?.classList.remove("ui-copy-disabled-faint"); } - const justifyTotal = y === 0 ? "justify-end" : ""; const fontWeight = y === 0 ? "font-semibold" : "font-normal"; - return `