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
181 changes: 160 additions & 21 deletions REFERENCE.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 35 additions & 6 deletions docs/contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,39 @@ return SQL text and declare `string` when exact numeric precision matters.

Use `operations.sampleCountsByGroup.queryNames` for context and evidence.

## Shared date ranges
### Bind datetime ranges

For period-driven analysis, declare `defineDateRangeContract()` from
`@altertable/data-app/contract` once and share it between the operation's parser
and `defineTimeView()`:
Declare a `dateTimeRange` parameter and pass a `DateTimeRange` to `query()`.
For a [comparison variable](variables.md#absolute-and-relative-datetime-ranges),
pass its `range` or `comparison` bounds. Reference those bounds in SQL:

```ts
const dataApp = defineDataApp({
// ... title, description, and scope
queries: {
events: {
statement: `SELECT occurred_at AS "occurredAt" FROM events
WHERE occurred_at >= $period.from AND occurred_at < $period.to`,
params: { period: { type: 'dateTimeRange' } },
projections: { occurredAt: { type: 'string' } },
},
},
});
```

The start is inclusive and the end exclusive. Relative values resolve at query
execution. Absolute values encode wall-clock fields in UTC: selecting 4 PM stores
16:00Z.
SQL bounds are timezone-free timestamps. Configure the backend session to the
reporting timezone and interpret timestamp-with-timezone comparisons in that
same zone.

## Share reporting policy

Pass an optional `defineDateRangeContract()` to `dateTimeRangeVariable()` to
share coverage, duration limits, and reporting timezone between the picker and
server parser. By default, coverage ends yesterday; set `completeDays: false`
to include today. Declare the policy once:

```ts
import { defineDateRangeContract } from '@altertable/data-app/contract';
Expand All @@ -98,5 +126,6 @@ const calendar = defineDateRangeContract({
});
```

Use `calendar.parseRequest()` to validate periods. The contract owns source
coverage, time zone, maximum range, and comparison rules.
Use the shared contract in the [datetime variable workflow](variables.md#absolute-and-relative-datetime-ranges),
including optional previous-period comparison. Calendar-only views share it with
`calendar.parseRequest()` and `defineTimeView()`.
7 changes: 7 additions & 0 deletions docs/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,10 @@ for inline forms.
Use `<Button variant="primary">` for the main committing action, such as Apply.
Use outline and ghost buttons for secondary actions.
Use `<Button variant="inline">` for actions within text or table cells. Use buttons for opening sheets and links for URL navigation.

## Datetime selection

Prefer a [datetime variable](variables.md#absolute-and-relative-datetime-ranges)
for generated controls and URL state. For custom screens, `<DateTimeRangePicker>`
accepts the same `DateTimeRange` and preset sets, with calendar and natural-language
entry. See its props in [REFERENCE.md](../REFERENCE.md).
61 changes: 61 additions & 0 deletions docs/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,64 @@ const dailyCountsView = defineTimeView({
Custom control ownership is needed only when generated controls cannot express
the requirement; use `/ui` for that case. Do not reuse reserved inspection,
presentation, or navigation URL keys.

## Absolute and relative datetime ranges

Use `dateTimeRangeVariable()` for times, rolling windows, or mixed absolute and
relative bounds. Share the variable between the operation parser and the view;
`<DataApp>` supplies the picker and URL state:

```ts
import {
dateTimeRangeVariable,
dateTimeRangePresets,
defineDateRangeContract,
variableInput,
} from '@altertable/data-app/contract';
import { createDataHooks } from '@altertable/data-app-react';

const calendar = defineDateRangeContract({
maxRangeDays: 366,
timeZone: 'UTC',
completeDays: false,
});
const period = dateTimeRangeVariable({
key: 'period',
contract: calendar,
comparison: true,
defaultValue: '30d',
presets: [
...dateTimeRangePresets.analytics,
{ label: 'Past 6 months', range: '6mo', shortcut: '6mo' },
],
});
const events = dataApp.defineOperation({
input: variableInput(() => ({}), { period }),
checks: [{ period: period.input(period.defaultValue) }],
policy: { maxQueryRows: 1000, maxDurationMs: 15000 },
async run({ query }, input) {
const current = await query('events', { period: input.period.range });
const previous = input.period.comparison
? await query('events', { period: input.period.comparison })
: null;
return { current: current.rows, previous: previous?.rows ?? null };
},
});
// ... client created from { events }, and eventsContext
const { defineTimeView } = createDataHooks(client);
const eventsView = defineTimeView({
operation: 'events',
dataContext: eventsContext,
time: period,
isEmpty: result => result.current.length === 0,
emptyFallback: { title: 'No events' },
});
```

Use `dateTimeRangePresets.monitoring` for shorter windows. Bind the query bounds
to a [datetime parameter](contract.md#bind-datetime-ranges).
`comparison: true` adds the toggle and supplies current/previous ranges resolved
against one reference time; the view derives metric comparison labels from that
request. Omit `comparison` for a plain `DateTimeRange` input, and omit `contract`
when no shared reporting policy is needed. Relative selections stay relative
across reloads.
17 changes: 17 additions & 0 deletions packages/data-app-react/src/authoring/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,20 @@ export type { DateDisplayProps } from '../ui/DateDisplay.tsx';
export { useDateFormatting } from '../date-formatting.ts';

export { ExternalLink, type ExternalLinkProps } from '../ui/ExternalLink.tsx';

export {
dateTimeRangeVariable,
dateTimeRangePresets,
type DateTimeRangePreset,
type DateTimeRangeVariable,
type DateTimeRangeComparisonVariable,
type DateTimeRangeComparisonVariableOptions,
type DateTimeRangeComparisonSelection,
type DateTimeRangeComparisonRequest,
type DateTimeRangeVariableOptions,
} from '../ui/variables.ts';

export {
DateTimeRangePicker,
type DateTimeRangePickerProps,
} from '../ui/DateTimeRangePicker.tsx';
15 changes: 10 additions & 5 deletions packages/data-app-react/src/bindings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { DateRangeRequest } from '@altertable/data-app/contract';
import type {
DateTimeRangeComparisonRequest,
DateRangeRequest,
} from '@altertable/data-app/contract';
import type { DateFormat } from '@altertable/data-app/format';
import { formatMetric } from '@altertable/data-app/format';
import type { MetricFormat } from '@altertable/data-app/format';
Expand Down Expand Up @@ -323,11 +326,11 @@ function datasetCsv<Data, Input>(

export class Metric<Data, Input> extends RegisteredBinding {
#select: (data: Data, input: Input) => MetricValues;
#date?: (input: Input) => DateRangeRequest;
#date?: (input: Input) => DateRangeRequest | DateTimeRangeComparisonRequest;
constructor(
readonly definition: MetricDefinition,
select: (data: Data, input: Input) => MetricValues,
date?: (input: Input) => DateRangeRequest,
date?: (input: Input) => DateRangeRequest | DateTimeRangeComparisonRequest,
owner?: object
) {
super(owner, definition.evidence);
Expand All @@ -339,7 +342,9 @@ export class Metric<Data, Input> extends RegisteredBinding {
return readSource<
Data,
Input,
MetricValues & { period?: DateRangeRequest },
MetricValues & {
period?: DateRangeRequest | DateTimeRangeComparisonRequest;
},
Source
>(source, (data, input) => {
const values = this.#select(data, input);
Expand Down Expand Up @@ -407,7 +412,7 @@ export class MetricRecords<Data, Input, DetailData, DetailInput> {
export function bindMetric<Data, Input>(
definition: MetricDefinition,
select: (data: Data, input: Input) => MetricValues,
date?: (input: Input) => DateRangeRequest,
date?: (input: Input) => DateRangeRequest | DateTimeRangeComparisonRequest,
owner?: object
) {
return new Metric(definition, select, date, owner);
Expand Down
21 changes: 18 additions & 3 deletions packages/data-app-react/src/exploration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { DateRangeRequest } from '@altertable/data-app/contract';
import type {
DateTimeRangeComparisonRequest,
DateRangeRequest,
} from '@altertable/data-app/contract';
import type {
AppVariable,
AppVariableValues,
Expand Down Expand Up @@ -32,7 +35,10 @@ type TargetDefinition = {
variables: VariableCollection;
input: (values: any) => unknown;
bindings?: ViewBindings<any, any>;
date?: { variable: string; input: (input: any) => DateRangeRequest };
date?: {
variable: string;
input: (input: any) => DateRangeRequest | DateTimeRangeComparisonRequest;
};
key?: string;
};
const targets = new WeakMap<object, TargetDefinition>();
Expand Down Expand Up @@ -83,7 +89,8 @@ function commit(definition: TargetDefinition, values: Record<string, unknown>) {
const resolved = Object.fromEntries(
Object.entries(definition.variables).map(([name, variable]) => [
name,
variable.kind === 'dateRange'
variable.kind === 'dateRange' ||
variable.kind === 'dateTimeRangeComparison'
? variable.input(values[name] as never)
: values[name],
])
Expand Down Expand Up @@ -114,6 +121,14 @@ export function targetAvailable(
return true;
}
const shown = readInput(definition, key, displayedInput);
if (variable.kind === 'dateTimeRangeComparison')
return (
!!shown &&
variable.same(
applied[key] as never,
(shown as DateTimeRangeComparisonRequest).selection
)
);
return variable.kind === 'dateRange'
? JSON.stringify(variable.input(applied[key] as never)) ===
JSON.stringify(shown)
Expand Down
Loading
Loading