Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 24 additions & 11 deletions web-common/src/features/canvas/components/kpi-grid/KPIGrid.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,35 @@
$: ({
specStore,
timeAndFilterStore,
parent: { name: canvasName },
parent: { name: canvasName, metricsView },
visible,
} = component);
$: kpiGridProperties = $specStore;
$: schema = validateKPIGridSchema(kpiGridProperties);

// Convert measures to KPI specs
$: kpis = (kpiGridProperties.measures || []).map((measure) => ({
metrics_view: kpiGridProperties.metrics_view,
measure,
sparkline: kpiGridProperties.sparkline,
hide_time_range: kpiGridProperties.hide_time_range,
comparison: kpiGridProperties.comparison,
dimension_filters: kpiGridProperties.dimension_filters,
time_filters: kpiGridProperties.time_filters,
}));
$: metricsViewQuery = metricsView.getMetricsViewFromName(
kpiGridProperties.metrics_view,
);
$: accessibleMeasureNames = new Set(
$metricsViewQuery.metricsView?.measures?.map((m) => m.name as string) ?? [],
);

// Convert measures to KPI specs, filtering out any excluded by a security
// policy (those will be absent from the metrics view's validSpec).
$: kpis = (kpiGridProperties.measures || [])
.filter(
(measure) =>
$metricsViewQuery.isLoading || accessibleMeasureNames.has(measure),
)
.map((measure) => ({
metrics_view: kpiGridProperties.metrics_view,
measure,
sparkline: kpiGridProperties.sparkline,
hide_time_range: kpiGridProperties.hide_time_range,
comparison: kpiGridProperties.comparison,
dimension_filters: kpiGridProperties.dimension_filters,
time_filters: kpiGridProperties.time_filters,
}));

$: filters = {
time_filters: kpiGridProperties.time_filters,
Expand Down
37 changes: 8 additions & 29 deletions web-common/src/features/canvas/components/leaderboard/selector.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import type { LeaderboardSpec } from "@rilldata/web-common/features/canvas/components/leaderboard";
import {
validateDimensions,
validateMeasures,
} from "@rilldata/web-common/features/canvas/components/validators";
import type { V1MetricsViewSpec } from "@rilldata/web-common/runtime-client";

export function validateLeaderboardSchema(
Expand Down Expand Up @@ -35,8 +31,14 @@ export function validateLeaderboardSchema(
const allDimensions =
metricsView?.dimensions?.map((d) => d.name || (d.column as string)) || [];

let measures = leaderboardSpec?.measures || [];
let dimensions = leaderboardSpec?.dimensions || [];
// Filter to only accessible fields, silently dropping any excluded by a
// security policy.
const measures = (leaderboardSpec?.measures || []).filter((m) =>
allMeasures.includes(m),
);
const dimensions = (leaderboardSpec?.dimensions || []).filter((d) =>
allDimensions.includes(d),
);

if (!measures.length || !dimensions.length) {
return {
Expand All @@ -45,29 +47,6 @@ export function validateLeaderboardSchema(
};
}

measures = measures.filter((c) => allMeasures.includes(c));
dimensions = dimensions.filter((c) => allDimensions.includes(c));

const validateMeasuresRes = validateMeasures(metricsView, measures);
if (!validateMeasuresRes.isValid) {
const invalidMeasures = validateMeasuresRes.invalidMeasures.join(", ");
return {
isValid: false,
error: `Invalid measure(s) "${invalidMeasures}" selected for the table`,
};
}

const validateDimensionsRes = validateDimensions(metricsView, dimensions);

if (!validateDimensionsRes.isValid) {
const invalidDimensions =
validateDimensionsRes.invalidDimensions.join(", ");

return {
isValid: false,
error: `Invalid dimension(s) "${invalidDimensions}" selected for the table`,
};
}
return {
isValid: true,
error: undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import type { PivotCanvasComponent } from "@rilldata/web-common/features/canvas/components/pivot";
import { isTimeDimension } from "@rilldata/web-common/features/dashboards/pivot/pivot-utils";
import ComponentHeader from "../../ComponentHeader.svelte";
import CanvasPivotRenderer from "./CanvasPivotRenderer.svelte";
import { validateTableSchema } from "./selector";
Expand Down Expand Up @@ -36,27 +37,70 @@

$: _metricViewSpec = getMetricsViewFromName(tableSpec.metrics_view);
$: metricsViewSpec = $_metricViewSpec.metricsView;
$: metricsViewLoading = $_metricViewSpec.isLoading;

$: schema = validateTableSchema(metricsViewSpec, tableSpec);
$: schema = validateTableSchema(metricsViewSpec, tableSpec, metricsViewLoading);
$: widthScopeKey = `canvas:${component.parent.name}:${component.id}`;

// Build accessible field lists by filtering out any fields not present in the
// metrics view spec (e.g. excluded by a security policy).
$: accessibleColumns =
"columns" in tableSpec
? (tableSpec.columns || []).filter((c) => {
const allMeasures =
metricsViewSpec?.measures?.map((m) => m.name as string) || [];
const allDimensions =
metricsViewSpec?.dimensions?.map(
(d) => d.name || (d.column as string),
) || [];
return allMeasures.includes(c) || allDimensions.includes(c);
})
: [];

$: accessibleMeasures =
!("columns" in tableSpec)
? (tableSpec.measures || []).filter((m) =>
metricsViewSpec?.measures?.some((mv) => mv.name === m),
)
: [];

$: accessibleRowDimensions =
!("columns" in tableSpec)
? (tableSpec.row_dimensions || []).filter(
(d) =>
metricsViewSpec?.dimensions?.some(
(mv) => mv.name === d || mv.column === d,
) ||
(metricsViewSpec?.timeDimension !== undefined &&
isTimeDimension(d, metricsViewSpec.timeDimension)),
)
: [];

$: accessibleColDimensions =
!("columns" in tableSpec)
? (tableSpec.col_dimensions || []).filter(
(d) =>
metricsViewSpec?.dimensions?.some(
(mv) => mv.name === d || mv.column === d,
) ||
(metricsViewSpec?.timeDimension !== undefined &&
isTimeDimension(d, metricsViewSpec.timeDimension)),
)
: [];

$: if ("columns" in tableSpec && schema.isValid) {
const columns = tableSpec?.columns || [];
pivotState.update((state) => ({
...state,
sorting: [],
expanded: {},
activeCell: null,
columnPage: 1,
rowPage: 1,
columns: tableFieldMapper(columns, metricsViewSpec),
columns: tableFieldMapper(accessibleColumns, metricsViewSpec),
showTotalsColumn: tableSpec.hide_totals_col !== true,
showTotalsRow: tableSpec.hide_totals_row !== true,
}));
} else if (!("columns" in tableSpec) && schema.isValid) {
const measures = tableSpec.measures || [];
const colDimensions = tableSpec.col_dimensions || [];
const rowDimensions = tableSpec.row_dimensions || [];
pivotState.update((state) => ({
...state,
sorting: [],
Expand All @@ -65,10 +109,10 @@
columnPage: 1,
rowPage: 1,
columns: [
...tableFieldMapper(colDimensions, metricsViewSpec),
...tableFieldMapper(measures, metricsViewSpec),
...tableFieldMapper(accessibleColDimensions, metricsViewSpec),
...tableFieldMapper(accessibleMeasures, metricsViewSpec),
],
rows: tableFieldMapper(rowDimensions, metricsViewSpec),
rows: tableFieldMapper(accessibleRowDimensions, metricsViewSpec),
showTotalsColumn: tableSpec.hide_totals_col !== true,
showTotalsRow: tableSpec.hide_totals_row !== true,
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
export let schema: {
isValid: boolean;
error?: string;
isLoading?: boolean;
};
export let pivotDataStore: PivotDataStore | undefined;
export let pivotConfig: Readable<PivotDataStoreConfig> | undefined;
Expand Down
90 changes: 32 additions & 58 deletions web-common/src/features/canvas/components/pivot/selector.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import {
validateDimensions,
validateMeasures,
} from "@rilldata/web-common/features/canvas/components/validators";
import { isTimeDimension } from "@rilldata/web-common/features/dashboards/pivot/pivot-utils";
import type { PivotSpec, TableSpec } from "./";
import type { PivotSpec, TableSpec } from ".";
import type { V1MetricsViewSpec } from "@rilldata/web-common/runtime-client";

export function validateTableSchema(
metricsView: V1MetricsViewSpec | undefined,
tableSpec: PivotSpec | TableSpec,
isLoading?: boolean,
): {
isValid: boolean;
error?: string;
isLoading?: boolean;
} {
if (isLoading) {
return { isValid: true, isLoading: true };
}

if (!metricsView) {
return {
isValid: false,
Expand All @@ -31,82 +33,54 @@ function validateFlat(tableSpec: TableSpec, metricsView: V1MetricsViewSpec) {
const allMeasures = metricsView?.measures?.map((m) => m.name as string) || [];
const allDimensions =
metricsView?.dimensions?.map((d) => d.name || (d.column as string)) || [];
const columns = tableSpec?.columns || [];

const measures = columns.filter((c) => allMeasures.includes(c));
const dimensions = columns.filter((c) => allDimensions.includes(c));
// Filter columns to only those accessible in the metrics view, silently
// dropping any excluded by a security policy.
const accessibleColumns = (tableSpec?.columns || []).filter(
(c) => allMeasures.includes(c) || allDimensions.includes(c),
);

if (!columns.length) {
if (!accessibleColumns.length) {
return {
isValid: false,
error: "Select at least one measure or dimension for the table",
};
}
const validateMeasuresRes = validateMeasures(metricsView, measures);
if (!validateMeasuresRes.isValid) {
const invalidMeasures = validateMeasuresRes.invalidMeasures.join(", ");
return {
isValid: false,
error: `Invalid measure(s) "${invalidMeasures}" selected for the table`,
};
}

const validateDimensionsRes = validateDimensions(metricsView, dimensions);

if (!validateDimensionsRes.isValid) {
const invalidDimensions =
validateDimensionsRes.invalidDimensions.join(", ");

return {
isValid: false,
error: `Invalid dimension(s) "${invalidDimensions}" selected for the table`,
};
}
return {
isValid: true,
error: undefined,
};
}

function validatePivot(tableSpec: PivotSpec, metricsView: V1MetricsViewSpec) {
const measures = tableSpec.measures || [];
const rowDimensions = tableSpec.row_dimensions || [];
const colDimensions = tableSpec.col_dimensions || [];
const allMeasures = metricsView?.measures?.map((m) => m.name as string) || [];
const allDimensions =
metricsView?.dimensions?.map((d) => d.name || (d.column as string)) || [];

// Filter each list to only accessible fields, silently dropping any
// excluded by a security policy.
const measures = (tableSpec.measures || []).filter((m) =>
allMeasures.includes(m),
);
const rowDimensions = (tableSpec.row_dimensions || []).filter(
(d) =>
allDimensions.includes(d) ||
(metricsView.timeDimension && isTimeDimension(d, metricsView.timeDimension)),
);
const colDimensions = (tableSpec.col_dimensions || []).filter(
(d) =>
allDimensions.includes(d) ||
(metricsView.timeDimension && isTimeDimension(d, metricsView.timeDimension)),
);

if (!measures.length && !rowDimensions.length && !colDimensions.length) {
return {
isValid: false,
error: "Select at least one measure or dimension for the table",
};
}
const validateMeasuresRes = validateMeasures(metricsView, measures);
if (!validateMeasuresRes.isValid) {
const invalidMeasures = validateMeasuresRes.invalidMeasures.join(", ");
return {
isValid: false,
error: `Invalid measure(s) "${invalidMeasures}" selected for the table`,
};
}

const allDimensions = rowDimensions
.concat(colDimensions)
.filter(
(d) =>
!metricsView.timeDimension ||
!isTimeDimension(d, metricsView.timeDimension),
);

const validateDimensionsRes = validateDimensions(metricsView, allDimensions);

if (!validateDimensionsRes.isValid) {
const invalidDimensions =
validateDimensionsRes.invalidDimensions.join(", ");

return {
isValid: false,
error: `Invalid dimension(s) "${invalidDimensions}" selected for the table`,
};
}
return {
isValid: true,
error: undefined,
Expand Down