|
| 1 | +import { z } from 'zod' |
| 2 | +import { |
| 3 | + aiExploreAggregations, |
| 4 | + basicExploreAggregations, |
| 5 | + exploreAggregations, |
| 6 | + exploreFilterTypesV2, |
| 7 | + filterableAiExploreDimensions, |
| 8 | + filterableBasicExploreDimensions, |
| 9 | + filterableExploreDimensions, |
| 10 | + granularityValues, |
| 11 | + queryableAiExploreDimensions, |
| 12 | + queryableBasicExploreDimensions, |
| 13 | + queryableExploreDimensions, |
| 14 | + relativeTimeRangeValuesV4, |
| 15 | + requestFilterTypeEmptyV2, |
| 16 | +} from './types' |
| 17 | + |
| 18 | +export const dashboardTileTypes = [ |
| 19 | + 'horizontal_bar', |
| 20 | + 'vertical_bar', |
| 21 | + 'gauge', |
| 22 | + 'donut', |
| 23 | + 'timeseries_line', |
| 24 | + 'timeseries_bar', |
| 25 | + 'golden_signals', |
| 26 | + 'top_n', |
| 27 | + 'slottable', |
| 28 | + 'single_value', |
| 29 | +] as const |
| 30 | +export type DashboardTileType = typeof dashboardTileTypes[number] |
| 31 | + |
| 32 | +// Common chart props |
| 33 | +const zSyntheticsDataKey = z.string() |
| 34 | +const zChartTitle = z.string() |
| 35 | +const zAllowCsvExport = z.boolean() |
| 36 | + |
| 37 | +// Can be either an array of strings or a { [key: string]: string } mapping |
| 38 | +const zChartDatasetColors = z.union([z.array(z.string()), z.record(z.string(), z.string())]) |
| 39 | + |
| 40 | +// Chart schemas |
| 41 | +export const zSlottableSchema = z.object({ |
| 42 | + type: z.literal('slottable'), |
| 43 | + id: z.string(), |
| 44 | +}).strict() |
| 45 | +export type SlottableOptions = z.infer<typeof zSlottableSchema> |
| 46 | + |
| 47 | +export const zBarChartSchema = z.object({ |
| 48 | + type: z.enum(['horizontal_bar', 'vertical_bar'] as const), |
| 49 | + stacked: z.boolean().optional(), |
| 50 | + chart_dataset_colors: zChartDatasetColors.optional(), |
| 51 | + synthetics_data_key: zSyntheticsDataKey.optional(), |
| 52 | + chart_title: zChartTitle.optional(), |
| 53 | + allow_csv_export: zAllowCsvExport.optional(), |
| 54 | +}).strict() |
| 55 | +export type BarChartOptions = z.infer<typeof zBarChartSchema> |
| 56 | + |
| 57 | +export const zTimeseriesChartSchema = z.object({ |
| 58 | + type: z.enum(['timeseries_line', 'timeseries_bar'] as const), |
| 59 | + stacked: z.boolean().optional(), |
| 60 | + threshold: z.record(z.string(), z.number()).optional(), |
| 61 | + chart_dataset_colors: zChartDatasetColors.optional(), |
| 62 | + synthetics_data_key: zSyntheticsDataKey.optional(), |
| 63 | + chart_title: zChartTitle.optional(), |
| 64 | + allow_csv_export: zAllowCsvExport.optional(), |
| 65 | +}).strict() |
| 66 | +export type TimeseriesChartOptions = z.infer<typeof zTimeseriesChartSchema> |
| 67 | + |
| 68 | +export const zGaugeChartSchema = z.object({ |
| 69 | + type: z.literal('gauge'), |
| 70 | + metric_display: z.enum(['hidden', 'single', 'full'] as const).optional(), |
| 71 | + reverse_dataset: z.boolean().optional(), |
| 72 | + numerator: z.number().optional(), |
| 73 | + synthetics_data_key: zSyntheticsDataKey.optional(), |
| 74 | + chart_title: zChartTitle.optional(), |
| 75 | +}).strict() |
| 76 | +export type GaugeChartOptions = z.infer<typeof zGaugeChartSchema> |
| 77 | + |
| 78 | +export const zDonutChartSchema = z.object({ |
| 79 | + type: z.literal('donut'), |
| 80 | + synthetics_data_key: zSyntheticsDataKey.optional(), |
| 81 | + chart_title: zChartTitle.optional(), |
| 82 | +}).strict() |
| 83 | +export type DonutChartOptions = z.infer<typeof zDonutChartSchema> |
| 84 | + |
| 85 | +export const zTopNTableSchema = z.object({ |
| 86 | + type: z.literal('top_n'), |
| 87 | + chart_title: zChartTitle.optional(), |
| 88 | + synthetics_data_key: zSyntheticsDataKey.optional(), |
| 89 | + description: z.string().optional(), |
| 90 | + entity_link: z.string().optional(), |
| 91 | +}).strict() |
| 92 | +export type TopNTableOptions = z.infer<typeof zTopNTableSchema> |
| 93 | + |
| 94 | +export const zMetricCardSchema = z.object({ |
| 95 | + type: z.literal('golden_signals'), |
| 96 | + chart_title: zChartTitle.optional(), |
| 97 | + long_card_titles: z.boolean().optional(), |
| 98 | + description: z.string().optional(), |
| 99 | + percentile_latency: z.boolean().optional(), |
| 100 | +}).strict() |
| 101 | +export type MetricCardOptions = z.infer<typeof zMetricCardSchema> |
| 102 | + |
| 103 | +export const zSingleValueSchema = z.object({ |
| 104 | + type: z.literal('single_value'), |
| 105 | + decimal_points: z.number().optional(), |
| 106 | + chart_title: zChartTitle.optional(), |
| 107 | +}).strict() |
| 108 | +export type SingleValueOptions = z.infer<typeof zSingleValueSchema> |
| 109 | + |
| 110 | +const zChartOptions = z.discriminatedUnion('type', [ |
| 111 | + zBarChartSchema, |
| 112 | + zGaugeChartSchema, |
| 113 | + zDonutChartSchema, |
| 114 | + zTimeseriesChartSchema, |
| 115 | + zMetricCardSchema, |
| 116 | + zTopNTableSchema, |
| 117 | + zSlottableSchema, |
| 118 | + zSingleValueSchema, |
| 119 | +]) |
| 120 | + |
| 121 | +export type ChartOptions = z.infer<typeof zChartOptions> |
| 122 | + |
| 123 | +const zExploreV4RelativeTime = z.object({ |
| 124 | + tz: z.string().optional().default('Etc/UTC'), |
| 125 | + type: z.literal('relative'), |
| 126 | + time_range: z.enum(relativeTimeRangeValuesV4).optional().default('1h'), |
| 127 | +}).strict() |
| 128 | + |
| 129 | +const zExploreV4AbsoluteTime = z.object({ |
| 130 | + tz: z.string().optional(), |
| 131 | + type: z.literal('absolute'), |
| 132 | + start: z.string(), |
| 133 | + end: z.string(), |
| 134 | +}).strict() |
| 135 | + |
| 136 | +const zTimeRange = z.discriminatedUnion('type', [zExploreV4RelativeTime, zExploreV4AbsoluteTime]).optional().default({ |
| 137 | + tz: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', |
| 138 | + type: 'relative', |
| 139 | + time_range: '1h', |
| 140 | +}) |
| 141 | + |
| 142 | +// Query helpers |
| 143 | +const zBaseQuery = z.object({ |
| 144 | + granularity: z.enum(granularityValues).optional(), |
| 145 | + time_range: zTimeRange.optional(), |
| 146 | + limit: z.number().optional(), |
| 147 | + meta: z.object().optional(), |
| 148 | +}) |
| 149 | + |
| 150 | +// metrics(dimensions) helpers |
| 151 | +const zMetrics = (aggs: readonly string[]) => z.array(z.enum(aggs as [string, ...string[]])).optional() |
| 152 | +const zDimensions = (dims: readonly string[]) => z.array(z.enum(dims as [string, ...string[]])).max(2).optional() |
| 153 | + |
| 154 | +// Filters helper: supports "in" and "empty" operators for a given field enum or union |
| 155 | +const zFilters = <T extends readonly string[]>(filterableDimensions: T) => { |
| 156 | + const zInFilter = z.object({ |
| 157 | + field: z.enum(filterableDimensions), |
| 158 | + operator: z.enum(exploreFilterTypesV2), |
| 159 | + value: z.array(z.union([z.string(), z.number(), z.null()])), |
| 160 | + }).strict() |
| 161 | + |
| 162 | + const zEmptyFilter = z.object({ |
| 163 | + field: z.enum(filterableDimensions), |
| 164 | + operator: z.enum(requestFilterTypeEmptyV2), |
| 165 | + }).strict() |
| 166 | + |
| 167 | + return z.array(z.discriminatedUnion('operator', [zInFilter, zEmptyFilter])).optional() |
| 168 | +} |
| 169 | + |
| 170 | +// Query schemas (per datasource) |
| 171 | +export const zApiUsageQuery = zBaseQuery.extend({ |
| 172 | + datasource: z.literal('api_usage'), |
| 173 | + metrics: zMetrics(exploreAggregations).optional(), |
| 174 | + dimensions: zDimensions(queryableExploreDimensions).optional(), |
| 175 | + filters: zFilters(filterableExploreDimensions).optional(), |
| 176 | +}).strict() |
| 177 | +export const zBasicQuery = zBaseQuery.extend({ |
| 178 | + datasource: z.literal('basic'), |
| 179 | + metrics: zMetrics(basicExploreAggregations).optional(), |
| 180 | + dimensions: zDimensions(queryableBasicExploreDimensions).optional(), |
| 181 | + filters: zFilters(filterableBasicExploreDimensions).optional(), |
| 182 | +}).strict() |
| 183 | +export const zLlmUsageQuery = zBaseQuery.extend({ |
| 184 | + datasource: z.literal('llm_usage'), |
| 185 | + metrics: zMetrics(aiExploreAggregations).optional(), |
| 186 | + dimensions: zDimensions(queryableAiExploreDimensions).optional(), |
| 187 | + filters: zFilters(filterableAiExploreDimensions).optional(), |
| 188 | +}).strict() |
| 189 | + |
| 190 | +export const zValidDashboardQuery = z.discriminatedUnion('datasource', [zApiUsageQuery, zBasicQuery, zLlmUsageQuery]) |
| 191 | +export type ValidDashboardQuery = z.infer<typeof zValidDashboardQuery> |
| 192 | + |
| 193 | +// Tile definition/layout/config schemas |
| 194 | +export const zTileDefinition = z.object({ |
| 195 | + query: zValidDashboardQuery, |
| 196 | + chart: zChartOptions, |
| 197 | +}).strict() |
| 198 | +export type TileDefinition = z.infer<typeof zTileDefinition> |
| 199 | + |
| 200 | +export const zTileLayout = z.object({ |
| 201 | + position: z.object({ |
| 202 | + col: z.number(), |
| 203 | + row: z.number(), |
| 204 | + }).strict(), |
| 205 | + size: z.object({ |
| 206 | + cols: z.number(), |
| 207 | + rows: z.number(), |
| 208 | + fit_to_content: z.boolean().optional(), |
| 209 | + }).strict(), |
| 210 | +}).strict() |
| 211 | +export type TileLayout = z.infer<typeof zTileLayout> |
| 212 | + |
| 213 | +export const zTileConfig = z.object({ |
| 214 | + type: z.enum(['chart']), |
| 215 | + definition: zTileDefinition, |
| 216 | + layout: zTileLayout, |
| 217 | + id: z.string().optional(), |
| 218 | +}).strict() |
| 219 | +export type TileConfig = z.infer<typeof zTileConfig> |
| 220 | + |
| 221 | +export const zDashboardConfig = z.object({ |
| 222 | + tiles: z.array(zTileConfig), |
| 223 | + tile_height: z.number().optional(), |
| 224 | + preset_filters: zFilters([ |
| 225 | + ...filterableExploreDimensions, |
| 226 | + ...filterableBasicExploreDimensions, |
| 227 | + ...filterableAiExploreDimensions, |
| 228 | + ]), |
| 229 | + template_id: z.string().nullable().optional(), |
| 230 | +}).strict() |
| 231 | +export type DashboardConfig = z.infer<typeof zDashboardConfig> |
| 232 | + |
| 233 | +export const parseDashboardConfig = (input: unknown) => zDashboardConfig.parse(input) |
0 commit comments