diff --git a/web-common/src/features/dashboards/dashboard-utils.ts b/web-common/src/features/dashboards/dashboard-utils.ts
index 9293d984305..3df04daad13 100644
--- a/web-common/src/features/dashboards/dashboard-utils.ts
+++ b/web-common/src/features/dashboards/dashboard-utils.ts
@@ -6,7 +6,7 @@ import type {
} from "@rilldata/web-common/runtime-client";
import type { TimeControlState } from "./time-controls/time-control-store";
import { getQuerySortType } from "./leaderboard/leaderboard-utils";
-import type { DashboardState_LeaderboardSortType } from "@rilldata/web-common/proto/gen/rill/ui/v1/dashboard_pb";
+import { SortType } from "./proto-state/derived-types";
export function isSummableMeasure(measure: MetricsViewSpecMeasureV2): boolean {
return (
@@ -31,10 +31,26 @@ export function prepareSortedQueryBody(
measureNames: string[],
timeControls: TimeControlState,
sortMeasureName: string,
- sortType: DashboardState_LeaderboardSortType,
+ sortType: SortType,
sortAscending: boolean,
filterForDimension: V1MetricsViewFilter
): QueryServiceMetricsViewComparisonToplistBody {
+ let comparisonTimeRange = {
+ start: timeControls.comparisonTimeStart,
+ end: timeControls.comparisonTimeEnd,
+ };
+
+ // FIXME: As a temporary way of enabling sorting by dimension values,
+ // Benjamin and Egor put in a patch that will allow us to use the
+ // dimension name as the measure name. This will need to be updated
+ // once they have stabilized the API.
+ if (sortType === SortType.DIMENSION) {
+ sortMeasureName = dimensionName;
+ // note also that we need to remove the comparison time range
+ // when sorting by dimension values, or the query errors
+ comparisonTimeRange = undefined;
+ }
+
const querySortType = getQuerySortType(sortType);
return {
@@ -44,10 +60,7 @@ export function prepareSortedQueryBody(
start: timeControls.timeStart,
end: timeControls.timeEnd,
},
- comparisonTimeRange: {
- start: timeControls.comparisonTimeStart,
- end: timeControls.comparisonTimeEnd,
- },
+ comparisonTimeRange,
sort: [
{
ascending: sortAscending,
diff --git a/web-common/src/features/dashboards/dimension-table/DimensionDisplay.svelte b/web-common/src/features/dashboards/dimension-table/DimensionDisplay.svelte
index 04ad59d75f1..eadf72b135a 100644
--- a/web-common/src/features/dashboards/dimension-table/DimensionDisplay.svelte
+++ b/web-common/src/features/dashboards/dimension-table/DimensionDisplay.svelte
@@ -21,13 +21,6 @@
} from "@rilldata/web-common/runtime-client";
import { useQueryClient } from "@tanstack/svelte-query";
import { runtime } from "../../../runtime-client/runtime-store";
-
- import { SortDirection, SortType } from "../proto-state/derived-types";
- import {
- metricsExplorerStore,
- useDashboardStore,
- } from "web-common/src/features/dashboards/stores/dashboard-stores";
-
import {
getDimensionFilterWithSearch,
prepareDimensionTableRows,
@@ -40,7 +33,7 @@
isSummableMeasure,
prepareSortedQueryBody,
} from "../dashboard-utils";
- import { LeaderboardContextColumn } from "../leaderboard-context-column";
+ import { metricsExplorerStore } from "../stores/dashboard-stores";
export let metricViewName: string;
export let dimensionName: string;
@@ -62,10 +55,15 @@
let dimension: MetricsViewDimension;
$: dimension = $dimensionQuery?.data;
$: dimensionColumn = getDimensionColumn(dimension);
+ const stateManagers = getStateManagers();
+ const timeControlsStore = useTimeControlStore(stateManagers);
- $: dashboardStore = useDashboardStore(metricViewName);
-
- const timeControlsStore = useTimeControlStore(getStateManagers());
+ const {
+ dashboardStore,
+ selectors: {
+ sorting: { sortedAscending },
+ },
+ } = stateManagers;
$: leaderboardMeasureName = $dashboardStore?.leaderboardMeasureName;
$: isBeingCompared =
@@ -101,8 +99,6 @@
$dashboardStore?.visibleMeasureKeys.has(m.name)
);
- $: sortAscending = $dashboardStore.sortDirection === SortDirection.ASCENDING;
-
$: totalsQuery = createQueryServiceMetricsViewTotals(
instanceId,
metricViewName,
@@ -130,15 +126,12 @@
}
$: columns = prepareVirtualizedDimTableColumns(
+ $dashboardStore,
allMeasures,
- leaderboardMeasureName,
referenceValues,
dimension,
- [...$dashboardStore.visibleMeasureKeys],
$timeControlsStore.showComparison,
- validPercentOfTotal,
- $dashboardStore.dashboardSortType,
- $dashboardStore.sortDirection
+ validPercentOfTotal
);
$: sortedQueryBody = prepareSortedQueryBody(
@@ -147,7 +140,7 @@
$timeControlsStore,
leaderboardMeasureName,
$dashboardStore.dashboardSortType,
- sortAscending,
+ $sortedAscending,
filterSet
);
@@ -178,39 +171,6 @@
metricsExplorerStore.toggleFilter(metricViewName, dimensionName, label);
}
- function onSortByColumn(event) {
- const columnName = event.detail;
-
- if (columnName === leaderboardMeasureName + "_delta") {
- metricsExplorerStore.toggleSort(metricViewName, SortType.DELTA_ABSOLUTE);
- metricsExplorerStore.setContextColumn(
- metricViewName,
- LeaderboardContextColumn.DELTA_ABSOLUTE
- );
- } else if (columnName === leaderboardMeasureName + "_delta_perc") {
- metricsExplorerStore.toggleSort(metricViewName, SortType.DELTA_PERCENT);
- metricsExplorerStore.setContextColumn(
- metricViewName,
- LeaderboardContextColumn.DELTA_PERCENT
- );
- } else if (columnName === leaderboardMeasureName + "_percent_of_total") {
- metricsExplorerStore.toggleSort(metricViewName, SortType.PERCENT);
- metricsExplorerStore.setContextColumn(
- metricViewName,
- LeaderboardContextColumn.PERCENT
- );
- } else if (columnName === leaderboardMeasureName) {
- metricsExplorerStore.toggleSort(metricViewName, SortType.VALUE);
- } else {
- metricsExplorerStore.setLeaderboardMeasureName(
- metricViewName,
- columnName
- );
- metricsExplorerStore.toggleSort(metricViewName, SortType.VALUE);
- metricsExplorerStore.setSortDescending(metricViewName);
- }
- }
-
function toggleComparisonDimension(dimensionName, isBeingCompared) {
metricsExplorerStore.setComparisonDimension(
metricViewName,
@@ -237,15 +197,14 @@
onSelectItem(event)}
- on:sort={(event) => onSortByColumn(event)}
on:toggle-dimension-comparison={() =>
toggleComparisonDimension(dimensionName, isBeingCompared)}
+ isFetching={$sortedQuery?.isFetching}
dimensionName={dimensionColumn}
{isBeingCompared}
{columns}
{selectedValues}
rows={tableRows}
- sortByColumn={leaderboardMeasureName}
{excludeMode}
/>
diff --git a/web-common/src/features/dashboards/dimension-table/DimensionHeader.svelte b/web-common/src/features/dashboards/dimension-table/DimensionHeader.svelte
index f6e57ed06b1..e588be5ef62 100644
--- a/web-common/src/features/dashboards/dimension-table/DimensionHeader.svelte
+++ b/web-common/src/features/dashboards/dimension-table/DimensionHeader.svelte
@@ -18,12 +18,24 @@
import Spinner from "../../entity-management/Spinner.svelte";
import { metricsExplorerStore } from "web-common/src/features/dashboards/stores/dashboard-stores";
import ExportDimensionTableDataButton from "./ExportDimensionTableDataButton.svelte";
+ import { getStateManagers } from "../state-managers/state-managers";
+ import { SortType } from "../proto-state/derived-types";
export let metricViewName: string;
export let dimensionName: string;
export let isFetching: boolean;
export let excludeMode = false;
+ const stateManagers = getStateManagers();
+ const {
+ selectors: {
+ sorting: { sortedByDimensionValue },
+ },
+ actions: {
+ sorting: { toggleSort },
+ },
+ } = stateManagers;
+
const queryClient = useQueryClient();
$: filterKey = excludeMode ? "exclude" : "include";
@@ -46,6 +58,9 @@
const goBackToLeaderboard = () => {
metricsExplorerStore.setMetricDimensionName(metricViewName, null);
+ if ($sortedByDimensionValue) {
+ toggleSort(SortType.VALUE);
+ }
};
function toggleFilterMode() {
cancelDashboardQueries(queryClient, metricViewName);
diff --git a/web-common/src/features/dashboards/dimension-table/DimensionTable.svelte b/web-common/src/features/dashboards/dimension-table/DimensionTable.svelte
index 2056abf0ee2..abb1eadf627 100644
--- a/web-common/src/features/dashboards/dimension-table/DimensionTable.svelte
+++ b/web-common/src/features/dashboards/dimension-table/DimensionTable.svelte
@@ -18,16 +18,25 @@ TableCells – the cell contents.
} from "./dimension-table-utils";
import type { DimensionTableRow } from "./dimension-table-types";
+ import { getStateManagers } from "../state-managers/state-managers";
+
const dispatch = createEventDispatcher();
export let rows: DimensionTableRow[];
export let columns: VirtualizedTableColumns[];
export let selectedValues: Array = [];
- export let sortByColumn: string;
export let dimensionName: string;
export let excludeMode = false;
export let isBeingCompared = false;
+ export let isFetching: boolean;
+
+ const {
+ actions: { dimTable },
+ selectors: {
+ sorting: { sortMeasure },
+ },
+ } = getStateManagers();
/** the overscan values tell us how much to render off-screen. These may be set by the consumer
* in certain circumstances. The tradeoff: the higher the overscan amount, the more DOM elements we have
@@ -169,7 +178,8 @@ TableCells – the cell contents.
async function handleColumnHeaderClick(event) {
colScrollOffset = $columnVirtualizer.scrollOffset;
- dispatch("sort", event.detail);
+ const columnName = event.detail;
+ dimTable.handleMeasureColumnHeaderClick(columnName);
}
async function handleResizeDimensionColumn(event) {
@@ -216,7 +226,7 @@ TableCells – the cell contents.
@@ -245,6 +255,7 @@ TableCells – the cell contents.
{excludeMode}
{scrolling}
{horizontalScrolling}
+ on:dimension-sort
on:select-item={(event) => onSelectItem(event)}
on:inspect={setActiveIndex}
/>
@@ -264,6 +275,10 @@ TableCells – the cell contents.
on:inspect={setActiveIndex}
cellLabel="Filter dimension value"
/>
+ {:else if isFetching}
+
+ Loading...
+
{:else}
No results to show
diff --git a/web-common/src/features/dashboards/dimension-table/DimensionValueHeader.svelte b/web-common/src/features/dashboards/dimension-table/DimensionValueHeader.svelte
index 2aba90342b6..cec14c971a1 100644
--- a/web-common/src/features/dashboards/dimension-table/DimensionValueHeader.svelte
+++ b/web-common/src/features/dashboards/dimension-table/DimensionValueHeader.svelte
@@ -4,6 +4,9 @@
import { createEventDispatcher, getContext } from "svelte";
import Cell from "../../../components/virtualized-table/core/Cell.svelte";
import type { VirtualizedTableConfig } from "../../../components/virtualized-table/types";
+ import ArrowDown from "@rilldata/web-common/components/icons/ArrowDown.svelte";
+ import { fly } from "svelte/transition";
+ import { getStateManagers } from "../state-managers/state-managers";
import type { ResizeEvent } from "@rilldata/web-common/components/virtualized-table/drag-table-cell";
const config: VirtualizedTableConfig = getContext("config");
@@ -21,6 +24,14 @@
export let activeIndex;
export let excludeMode = false;
+ const {
+ actions: {
+ sorting: { sortByDimensionValue },
+ },
+ selectors: {
+ sorting: { sortedByDimensionValue, sortedAscending },
+ },
+ } = getStateManagers();
const dispatch = createEventDispatcher();
$: atLeastOneSelected = !!selectedIndex?.length;
@@ -54,10 +65,29 @@
enableResize={true}
position="top-left"
borderRight={horizontalScrolling}
- bgClass="bg-white"
+ bgClass={$sortedByDimensionValue ? `bg-gray-50` : "bg-white"}
+ on:click={sortByDimensionValue}
+ on:keydown={sortByDimensionValue}
on:resize={handleResize}
>
-
{column?.label || column?.name}
+
+
{column?.label || column?.name}
+ {#if $sortedByDimensionValue}
+
+ {#if $sortedAscending}
+
+ {:else}
+
+ {/if}
+
+ {/if}
+
{#each virtualRowItems as row (`row-${row.key}`)}
{@const rowActive = activeIndex === row?.index}
@@ -66,7 +96,7 @@
position="left"
header={{ size: width, start: row.start }}
borderRight={horizontalScrolling}
- bgClass="bg-white"
+ bgClass={$sortedByDimensionValue ? `bg-gray-50` : "bg-white"}
>
m.name);
+ const leaderboardMeasureName = dash.leaderboardMeasureName;
const selectedMeasure = allMeasures.find(
(m) => m.name === leaderboardMeasureName
);
const dimensionColumn = getDimensionColumn(dimension);
// copy column names so we don't mutate the original
- const columnNames = [...inputColumnNames];
-
- addContextColumnNames(
- columnNames,
- timeComparison,
- validPercentOfTotal,
- selectedMeasure
- );
+ const columnNames = [...dash.visibleMeasureKeys];
+
+ // don't add context columns if sorting by dimension
+ if (sortType !== SortType.DIMENSION) {
+ addContextColumnNames(
+ columnNames,
+ timeComparison,
+ validPercentOfTotal,
+ selectedMeasure
+ );
+ }
// Make dimension the first column
columnNames.unshift(dimensionColumn);
return columnNames
.map((name) => {
- const highlight =
- name === selectedMeasure.name ||
- name.endsWith("_delta") ||
- name.endsWith("_delta_perc") ||
- name.endsWith("_percent_of_total");
+ let highlight = false;
+ if (sortType === SortType.DIMENSION) {
+ highlight = name === dimensionColumn;
+ } else {
+ highlight =
+ name === selectedMeasure.name ||
+ name.endsWith("_delta") ||
+ name.endsWith("_delta_perc") ||
+ name.endsWith("_percent_of_total");
+ }
let sorted = undefined;
if (name.endsWith("_delta") && sortType === SortType.DELTA_ABSOLUTE) {
diff --git a/web-common/src/features/dashboards/state-managers/actions/context-columns.ts b/web-common/src/features/dashboards/state-managers/actions/context-columns.ts
new file mode 100644
index 00000000000..2686f9d63fc
--- /dev/null
+++ b/web-common/src/features/dashboards/state-managers/actions/context-columns.ts
@@ -0,0 +1,42 @@
+import { LeaderboardContextColumn } from "../../leaderboard-context-column";
+import { sortTypeForContextColumnType } from "../../stores/dashboard-stores";
+import type { MetricsExplorerEntity } from "../../stores/metrics-explorer-entity";
+
+export const setContextColumn = (
+ metricsExplorer: MetricsExplorerEntity,
+ contextColumn: LeaderboardContextColumn
+) => {
+ const initialSort = sortTypeForContextColumnType(
+ metricsExplorer.leaderboardContextColumn
+ );
+ switch (contextColumn) {
+ case LeaderboardContextColumn.DELTA_ABSOLUTE:
+ case LeaderboardContextColumn.DELTA_PERCENT: {
+ // if there is no time comparison, then we can't show
+ // these context columns, so return with no change
+ if (metricsExplorer.showTimeComparison === false) return;
+
+ metricsExplorer.leaderboardContextColumn = contextColumn;
+ break;
+ }
+ default:
+ metricsExplorer.leaderboardContextColumn = contextColumn;
+ }
+
+ // if we have changed the context column, and the leaderboard is
+ // sorted by the context column from before we made the change,
+ // then we also need to change
+ // the sort type to match the new context column
+ if (metricsExplorer.dashboardSortType === initialSort) {
+ metricsExplorer.dashboardSortType =
+ sortTypeForContextColumnType(contextColumn);
+ }
+};
+
+export const contextColActions = {
+ /**
+ * Updates the dashboard to use the context column of the given type,
+ * as well as updating to sort by that context column.
+ */
+ setContextColumn,
+};
diff --git a/web-common/src/features/dashboards/state-managers/actions/core-actions.ts b/web-common/src/features/dashboards/state-managers/actions/core-actions.ts
new file mode 100644
index 00000000000..8597c600a93
--- /dev/null
+++ b/web-common/src/features/dashboards/state-managers/actions/core-actions.ts
@@ -0,0 +1,8 @@
+import type { MetricsExplorerEntity } from "../../stores/metrics-explorer-entity";
+
+export const setLeaderboardMeasureName = (
+ dash: MetricsExplorerEntity,
+ name: string
+) => {
+ dash.leaderboardMeasureName = name;
+};
diff --git a/web-common/src/features/dashboards/state-managers/actions/dimension-table.ts b/web-common/src/features/dashboards/state-managers/actions/dimension-table.ts
new file mode 100644
index 00000000000..67be5ab31a0
--- /dev/null
+++ b/web-common/src/features/dashboards/state-managers/actions/dimension-table.ts
@@ -0,0 +1,34 @@
+import { SortType } from "../../proto-state/derived-types";
+import { toggleSort, sortActions } from "./sorting";
+import { LeaderboardContextColumn } from "../../leaderboard-context-column";
+import { setContextColumn } from "./context-columns";
+import type { MetricsExplorerEntity } from "../../stores/metrics-explorer-entity";
+import { setLeaderboardMeasureName } from "./core-actions";
+
+export const handleMeasureColumnHeaderClick = (
+ dash: MetricsExplorerEntity,
+ measureName: string
+) => {
+ const { leaderboardMeasureName: name } = dash;
+
+ if (measureName === name + "_delta") {
+ toggleSort(dash, SortType.DELTA_ABSOLUTE);
+ setContextColumn(dash, LeaderboardContextColumn.DELTA_ABSOLUTE);
+ } else if (measureName === name + "_delta_perc") {
+ toggleSort(dash, SortType.DELTA_PERCENT);
+ setContextColumn(dash, LeaderboardContextColumn.DELTA_PERCENT);
+ } else if (measureName === name + "_percent_of_total") {
+ toggleSort(dash, SortType.PERCENT);
+ setContextColumn(dash, LeaderboardContextColumn.PERCENT);
+ } else if (measureName === name) {
+ toggleSort(dash, SortType.VALUE);
+ } else {
+ setLeaderboardMeasureName(dash, measureName);
+ toggleSort(dash, SortType.VALUE);
+ sortActions.setSortDescending(dash);
+ }
+};
+
+export const dimTableActions = {
+ handleMeasureColumnHeaderClick,
+};
diff --git a/web-common/src/features/dashboards/state-managers/actions/index.ts b/web-common/src/features/dashboards/state-managers/actions/index.ts
new file mode 100644
index 00000000000..f8047b4f2cb
--- /dev/null
+++ b/web-common/src/features/dashboards/state-managers/actions/index.ts
@@ -0,0 +1,69 @@
+import { sortActions } from "./sorting";
+import { contextColActions } from "./context-columns";
+import type { MetricsExplorerEntity } from "../../stores/metrics-explorer-entity";
+import { setLeaderboardMeasureName } from "./core-actions";
+import { dimTableActions } from "./dimension-table";
+import type {
+ DashboardCallbackExecutor,
+ DashboardMutatorFn,
+ DashboardMutatorFns,
+ DashboardUpdaters,
+} from "./types";
+
+export type StateManagerActions = ReturnType;
+
+export const createStateManagerActions = (
+ updateDashboard: DashboardCallbackExecutor
+) => {
+ return {
+ /**
+ * Actions related to the sorting state of the dashboard.
+ */
+ sorting: createDashboardUpdaters(updateDashboard, sortActions),
+ /**
+ * Actions related to the dashboard context columns.
+ */
+ contextCol: createDashboardUpdaters(updateDashboard, contextColActions),
+ /**
+ * Actions related to the dimension table.
+ */
+ dimTable: createDashboardUpdaters(updateDashboard, dimTableActions),
+ // Note: for now, some core actions are kept in the root of the
+ // actions object. Can revisit that later if we want to move them.
+ setLeaderboardMeasureName: dashboardMutatorToUpdater(
+ updateDashboard,
+ setLeaderboardMeasureName
+ ),
+ };
+};
+
+/**
+ * `dashboardMutatorToUpdater` take a DashboardCallbackExecutor
+ * and returns a DashboardMutatorFn that directly updates the dashboard
+ * by calling the DashboardCallbackExecutor.
+ **/
+function dashboardMutatorToUpdater(
+ updateDashboard: DashboardCallbackExecutor,
+ mutator: DashboardMutatorFn
+): (...params: T) => void {
+ return (...x) => {
+ const callback = (dash: MetricsExplorerEntity) => mutator(dash, ...x);
+ updateDashboard(callback);
+ };
+}
+
+/**
+ * Takes an object containing `DashboardMutatorFn`s,
+ * and returns an object of functions that directly update the dashboard.
+ */
+function createDashboardUpdaters(
+ updateDashboard: DashboardCallbackExecutor,
+ mutators: T
+): DashboardUpdaters {
+ return Object.fromEntries(
+ Object.entries(mutators).map(([key, mutator]) => [
+ key,
+ dashboardMutatorToUpdater(updateDashboard, mutator),
+ ])
+ ) as DashboardUpdaters;
+}
diff --git a/web-common/src/features/dashboards/state-managers/actions/sorting.ts b/web-common/src/features/dashboards/state-managers/actions/sorting.ts
new file mode 100644
index 00000000000..78f1f3d3238
--- /dev/null
+++ b/web-common/src/features/dashboards/state-managers/actions/sorting.ts
@@ -0,0 +1,46 @@
+import { SortDirection, SortType } from "../../proto-state/derived-types";
+import type { MetricsExplorerEntity } from "../../stores/metrics-explorer-entity";
+
+export const toggleSort = (
+ metricsExplorer: MetricsExplorerEntity,
+ sortType: SortType
+) => {
+ // if sortType is not provided, or if it is provided
+ // and is the same as the current sort type,
+ // then just toggle the current sort direction
+ if (
+ sortType === undefined ||
+ metricsExplorer.dashboardSortType === sortType
+ ) {
+ metricsExplorer.sortDirection =
+ metricsExplorer.sortDirection === SortDirection.ASCENDING
+ ? SortDirection.DESCENDING
+ : SortDirection.ASCENDING;
+ } else {
+ // if the sortType is different from the current sort type,
+ // then update the sort type and set the sort direction
+ // to descending
+ metricsExplorer.dashboardSortType = sortType;
+ metricsExplorer.sortDirection = SortDirection.DESCENDING;
+ }
+};
+
+export const sortActions = {
+ /**
+ * Sets the sort type for the dashboard (value, percent, delta, etc.)
+ */
+ toggleSort,
+ /**
+ * Sets the dashboard to be sorted by dimension value.
+ * Note that this should only be used in the dimension table
+ */
+ sortByDimensionValue: (metricsExplorer: MetricsExplorerEntity) =>
+ toggleSort(metricsExplorer, SortType.DIMENSION),
+
+ /**
+ * Sets the sort direction to descending.
+ */
+ setSortDescending: (metricsExplorer: MetricsExplorerEntity) => {
+ metricsExplorer.sortDirection = SortDirection.DESCENDING;
+ },
+};
diff --git a/web-common/src/features/dashboards/state-managers/actions/types.ts b/web-common/src/features/dashboards/state-managers/actions/types.ts
new file mode 100644
index 00000000000..7b01e56eecc
--- /dev/null
+++ b/web-common/src/features/dashboards/state-managers/actions/types.ts
@@ -0,0 +1,57 @@
+import type { MetricsExplorerEntity } from "../../stores/metrics-explorer-entity";
+
+// Note: the types below are helper types to simplify the type inference
+// used in the creation of StateManagerActions, so that we can have nice
+// autocomplete and type checking in the IDE, while still keeping the
+// code that is used to define actions organized and readable.
+
+/**
+ * A DashboardMutatorCallback is a function that mutates
+ * a MetricsExplorerEntity, i.e., the data single dashboard.
+ * This will often be a closure over other parameters
+ * that are relevant to the mutation.
+ */
+export type DashboardMutatorCallback = (
+ metricsExplorer: MetricsExplorerEntity
+) => void;
+
+/**
+ * DashboardCallbackExecutor is a function that takes a
+ * DashboardMutatorCallback and executes it. The
+ * DashboardCallbackExecutor is a closure containing a reference
+ * to the live dashboard, and therefore calling this function
+ * on a DashboardMutatorCallback will actually update the dashboard.
+ */
+export type DashboardCallbackExecutor = (
+ callback: DashboardMutatorCallback
+) => void;
+
+/**
+ * A DashboardMutatorFn is a function mutates the data
+ * model of a single dashboard.
+ * It takes a reference to a dashboard as its first parameter,
+ * and may take any number of additional parameters relevant to the mutation.
+ */
+export type DashboardMutatorFn = (
+ dash: MetricsExplorerEntity,
+ ...params: T
+) => void;
+
+export type DashboardMutatorFns = {
+ [key: string]: DashboardMutatorFn;
+};
+
+/**
+ * A helper type that drops the first element from a tuple.
+ */
+type DropFirst = T extends [unknown, ...infer U]
+ ? U
+ : never;
+
+/**
+ * A DashboardUpdaters object is a collection of functions that
+ * directly update the live dashboard.
+ */
+export type DashboardUpdaters = Expand<{
+ [P in keyof T]: (...params: DropFirst>) => void;
+}>;
diff --git a/web-common/src/features/dashboards/state-managers/selectors/index.ts b/web-common/src/features/dashboards/state-managers/selectors/index.ts
new file mode 100644
index 00000000000..6b832e6f24c
--- /dev/null
+++ b/web-common/src/features/dashboards/state-managers/selectors/index.ts
@@ -0,0 +1,31 @@
+import { sortingSelectors } from "./sorting";
+import { derived, type Readable } from "svelte/store";
+import type { MetricsExplorerEntity } from "../../stores/metrics-explorer-entity";
+import type { ReadablesObj, SelectorFnsObj } from "./types";
+
+export type StateManagerReadables = ReturnType<
+ typeof createStateManagerReadables
+>;
+
+export const createStateManagerReadables = (
+ dashboardStore: Readable
+) => {
+ return {
+ /**
+ * Readables related to the sorting state of the dashboard.
+ */
+ sorting: createReadablesFromSelectors(sortingSelectors, dashboardStore),
+ };
+};
+
+function createReadablesFromSelectors(
+ selectors: T,
+ dashboardStore: Readable
+): ReadablesObj {
+ return Object.fromEntries(
+ Object.entries(selectors).map(([key, selectorFn]) => [
+ key,
+ derived(dashboardStore, selectorFn),
+ ])
+ ) as ReadablesObj;
+}
diff --git a/web-common/src/features/dashboards/state-managers/selectors/sorting.ts b/web-common/src/features/dashboards/state-managers/selectors/sorting.ts
new file mode 100644
index 00000000000..ed4b47ac4f0
--- /dev/null
+++ b/web-common/src/features/dashboards/state-managers/selectors/sorting.ts
@@ -0,0 +1,31 @@
+import { SortDirection, SortType } from "../../proto-state/derived-types";
+import type { MetricsExplorerEntity } from "../../stores/metrics-explorer-entity";
+
+export const sortingSelectors = {
+ /**
+ * Gets the sort type for the dash (value, percent, delta, etc.)
+ */
+ sortType: (dashboard: MetricsExplorerEntity) => dashboard.dashboardSortType,
+
+ /**
+ * true if the dashboard is sorted ascending, false otherwise.
+ */
+ sortedAscending: (dashboard: MetricsExplorerEntity) =>
+ dashboard.sortDirection === SortDirection.ASCENDING,
+
+ /**
+ * Returns the measure name that the dashboard is sorted by,
+ * or null if the dashboard is sorted by dimension value.
+ */
+ sortMeasure: (dashboard: MetricsExplorerEntity) =>
+ dashboard.dashboardSortType !== SortType.DIMENSION &&
+ dashboard.dashboardSortType !== SortType.UNSPECIFIED
+ ? dashboard.leaderboardMeasureName
+ : null,
+
+ /**
+ * Returns true if the dashboard is sorted by a dimension, false otherwise.
+ */
+ sortedByDimensionValue: (dashboard: MetricsExplorerEntity) =>
+ dashboard.dashboardSortType === SortType.DIMENSION,
+};
diff --git a/web-common/src/features/dashboards/state-managers/selectors/types.ts b/web-common/src/features/dashboards/state-managers/selectors/types.ts
new file mode 100644
index 00000000000..455fc07d998
--- /dev/null
+++ b/web-common/src/features/dashboards/state-managers/selectors/types.ts
@@ -0,0 +1,24 @@
+import type { Readable } from "svelte/store";
+import type { MetricsExplorerEntity } from "../../stores/metrics-explorer-entity";
+
+/**
+ * A SelectorFn is a pure function that takes dashboard data
+ * (a MetricsExplorerEntity) and returns some derived value from it.
+ */
+export type SelectorFn = (dashboard: MetricsExplorerEntity) => T;
+
+/**
+ * A SelectorFnsObj object is a collection of pure SelectorFn functions.
+ */
+export type SelectorFnsObj = {
+ [key: string]: SelectorFn;
+};
+
+/**
+ * A ReadablesObj object is a collection readables that are connected
+ * to the live dashboard store and can be
+ * used to select data from the dashboard.
+ */
+export type ReadablesObj = Expand<{
+ [P in keyof T]: Readable>;
+}>;
diff --git a/web-common/src/features/dashboards/state-managers/state-managers.ts b/web-common/src/features/dashboards/state-managers/state-managers.ts
index 5a4353e821b..1bbee34e9cb 100644
--- a/web-common/src/features/dashboards/state-managers/state-managers.ts
+++ b/web-common/src/features/dashboards/state-managers/state-managers.ts
@@ -10,6 +10,12 @@ import {
useDashboardStore,
} from "web-common/src/features/dashboards/stores/dashboard-stores";
import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
+import {
+ StateManagerReadables,
+ createStateManagerReadables,
+} from "./selectors";
+import { createStateManagerActions, type StateManagerActions } from "./actions";
+import type { DashboardCallbackExecutor } from "./actions/types";
export type StateManagers = {
runtime: Writable;
@@ -18,9 +24,15 @@ export type StateManagers = {
dashboardStore: Readable;
queryClient: QueryClient;
setMetricsViewName: (s: string) => void;
- updateDashboard: (
- callback: (metricsExplorer: MetricsExplorerEntity) => void
- ) => void;
+ updateDashboard: DashboardCallbackExecutor;
+ /**
+ * A collection of Readables that can be used to select data from the dashboard.
+ */
+ selectors: StateManagerReadables;
+ /**
+ * A collection of functions that update the dashboard data model.
+ */
+ actions: StateManagerActions;
};
export const DEFAULT_STORE_KEY = Symbol("state-managers");
@@ -63,6 +75,8 @@ export function createStateManagers({
metricsViewNameStore.set(name);
},
updateDashboard,
+ selectors: createStateManagerReadables(dashboardStore),
+ actions: createStateManagerActions(updateDashboard),
};
}
diff --git a/web-common/src/features/dashboards/stores/dashboard-stores.ts b/web-common/src/features/dashboards/stores/dashboard-stores.ts
index 65a92fe8cbf..905bb16f994 100644
--- a/web-common/src/features/dashboards/stores/dashboard-stores.ts
+++ b/web-common/src/features/dashboards/stores/dashboard-stores.ts
@@ -550,7 +550,7 @@ function setDisplayComparison(
}
}
-function sortTypeForContextColumnType(
+export function sortTypeForContextColumnType(
contextCol: LeaderboardContextColumn
): SortType {
const sortType = {
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 99a51c0d7e0..24cb0dfed00 100644
--- a/web-common/src/features/dashboards/stores/metrics-explorer-entity.ts
+++ b/web-common/src/features/dashboards/stores/metrics-explorer-entity.ts
@@ -11,72 +11,106 @@ import type { V1MetricsViewFilter } from "@rilldata/web-common/runtime-client";
export interface MetricsExplorerEntity {
name: string;
- // selected measure names to be shown
+ /**
+ * selected measure names to be shown
+ */
selectedMeasureNames: Array;
- // This array controls which measures are visible in
- // explorer on the client. Note that this will need to be
- // updated to include all measure keys upon initialization
- // or else all measure will be hidden
+ /**
+ * This array controls which measures are visible in
+ * explorer on the client. Note that this will need to be
+ * updated to include all measure keys upon initialization
+ * or else all measure will be hidden
+ */
visibleMeasureKeys: Set;
- // While the `visibleMeasureKeys` has the list of visible measures,
- // this is explicitly needed to fill the state.
- // TODO: clean this up when we refactor how url state is synced
+
+ /**
+ * While the `visibleMeasureKeys` has the list of visible measures,
+ * this is explicitly needed to fill the state.
+ * TODO: clean this up when we refactor how url state is synced
+ */
allMeasuresVisible: boolean;
- // This array controls which dimensions are visible in
- // explorer on the client.Note that if this is null, all
- // dimensions will be visible (this is needed to default to all visible
- // when there are not existing keys in the URL or saved on the
- // server)
+ /**
+ * This array controls which dimensions are visible in
+ * explorer on the client.Note that if this is null, all
+ * dimensions will be visible (this is needed to default to all visible
+ * when there are not existing keys in the URL or saved on the
+ * server)
+ */
visibleDimensionKeys: Set;
- // While the `visibleDimensionKeys` has the list of all visible dimensions,
- // this is explicitly needed to fill the state.
- // TODO: clean this up when we refactor how url state is synced
+
+ /**
+ * While the `visibleDimensionKeys` has the list of all visible dimensions,
+ * this is explicitly needed to fill the state.
+ * TODO: clean this up when we refactor how url state is synced
+ */
allDimensionsVisible: boolean;
- // This is the name of the primary active measure in the dashboard.
- // This is the measure that will be shown in leaderboards, and
- // will be used for sorting the leaderboard and dimension
- // detail table.
- // This "name" is the internal name of the measure from the YAML,
- // not the human readable name.
+ /**
+ * This is the name of the primary active measure in the dashboard.
+ * This is the measure that will be shown in leaderboards, and
+ * will be used for sorting the leaderboard and dimension detail table.
+ * This "name" is the internal name of the measure from the YAML,
+ * not the human readable name.
+ */
leaderboardMeasureName: string;
- // This is the sort type that will be used for the leaderboard
- // and dimension detail table. See SortType for more details.
+ /**
+ * This is the sort type that will be used for the leaderboard
+ * and dimension detail table. See SortType for more details.
+ */
dashboardSortType: SortType;
- // This is the sort direction that will be used for the leaderboard
- // and dimension detail table.
+
+ /**
+ * This is the sort direction that will be used for the leaderboard
+ * and dimension detail table.
+ */
sortDirection: SortDirection;
filters: V1MetricsViewFilter;
- // stores whether a dimension is in include/exclude filter mode
- // false/absence = include, true = exclude
+
+ /**
+ * stores whether a dimension is in include/exclude filter mode
+ * false/absence = include, true = exclude
+ */
dimensionFilterExcludeMode: Map;
- // user selected time range
+
+ /**
+ * user selected time range
+ */
selectedTimeRange?: DashboardTimeControls;
- // user selected scrub range
+ /**
+ * user selected scrub range
+ */
selectedScrubRange?: ScrubRange;
lastDefinedScrubRange?: ScrubRange;
selectedComparisonTimeRange?: DashboardTimeControls;
selectedComparisonDimension?: string;
- // user selected timezone
+ /**
+ * user selected timezone
+ */
selectedTimezone?: string;
- // flag to show/hide time comparison based on user preference.
- // This controls whether a time comparison is shown in e.g.
- // the line charts and bignums.
- // It does NOT affect the leaderboard context column.
+ /**
+ * flag to show/hide time comparison based on user preference.
+ * This controls whether a time comparison is shown in e.g.
+ * the line charts and bignums.
+ * It does NOT affect the leaderboard context column.
+ */
showTimeComparison?: boolean;
- // state of context column in the leaderboard
+ /**
+ * state of context column in the leaderboard
+ */
leaderboardContextColumn: LeaderboardContextColumn;
- // user selected dimension
+ /**
+ * user selected dimension
+ */
selectedDimensionName?: string;
proto?: string;
|