mirror of https://github.com/buster-so/buster.git
534 lines
18 KiB
TypeScript
534 lines
18 KiB
TypeScript
import type { FileRouteTypes } from '@/routeTree.gen';
|
|
import type { OptionsTo } from '@/types/routes';
|
|
|
|
type RouteFilePaths = FileRouteTypes['to'];
|
|
|
|
/**
|
|
* Type definitions for asset route parameters
|
|
*/
|
|
type ChatParamsToRoute = {
|
|
assetType: 'chat';
|
|
assetId: string;
|
|
metricId?: string;
|
|
dashboardId?: string;
|
|
reportId?: string;
|
|
dashboardVersionNumber?: number;
|
|
metricVersionNumber?: number;
|
|
reportVersionNumber?: number;
|
|
};
|
|
|
|
type MetricParamsToRoute = {
|
|
assetType: 'metric';
|
|
assetId: string;
|
|
dashboardId?: string;
|
|
reportId?: string;
|
|
chatId?: string;
|
|
versionNumber?: number;
|
|
};
|
|
|
|
type DashboardParamsToRoute = {
|
|
assetType: 'dashboard';
|
|
assetId: string;
|
|
metricId?: string;
|
|
reportId?: string;
|
|
chatId?: string;
|
|
versionNumber?: number;
|
|
metricVersionNumber?: number;
|
|
};
|
|
|
|
type ReportParamsToRoute = {
|
|
assetType: 'report';
|
|
assetId: string;
|
|
metricId?: string;
|
|
chatId?: string;
|
|
versionNumber?: number;
|
|
metricVersionNumber?: number;
|
|
};
|
|
|
|
type CollectionParamsToRoute = {
|
|
assetType: 'collection';
|
|
assetId: string;
|
|
chatId?: string;
|
|
metricId?: string;
|
|
dashboardId?: string;
|
|
metricVersionNumber?: number;
|
|
dashboardVersionNumber?: number;
|
|
};
|
|
|
|
export type AssetParamsToRoute =
|
|
| ChatParamsToRoute
|
|
| MetricParamsToRoute
|
|
| DashboardParamsToRoute
|
|
| ReportParamsToRoute
|
|
| CollectionParamsToRoute;
|
|
|
|
/**
|
|
* Route builder internal state type
|
|
*/
|
|
type RouteBuilderState = {
|
|
collectionId?: string;
|
|
chatId?: string;
|
|
metricId?: string;
|
|
dashboardId?: string;
|
|
reportId?: string;
|
|
// Version numbers for search parameters
|
|
versionNumber?: number;
|
|
metricVersionNumber?: number;
|
|
dashboardVersionNumber?: number;
|
|
reportVersionNumber?: number;
|
|
};
|
|
|
|
/**
|
|
* Type-safe route mapping based on parameter combinations
|
|
*/
|
|
const ROUTE_MAP: Record<string, RouteFilePaths> = {
|
|
// Single asset routes
|
|
chat: '/app/chats/$chatId',
|
|
dashboard: '/app/dashboards/$dashboardId',
|
|
metric: '/app/metrics/$metricId',
|
|
report: '/app/reports/$reportId',
|
|
collection: '/app/collections/$collectionId',
|
|
|
|
// Direct asset combination routes
|
|
'dashboard+metric': '/app/dashboards/$dashboardId/metrics/$metricId',
|
|
'report+metric': '/app/reports/$reportId/metrics/$metricId',
|
|
|
|
// Chat combination routes
|
|
'chat+dashboard': '/app/chats/$chatId/dashboards/$dashboardId',
|
|
'chat+metric': '/app/chats/$chatId/metrics/$metricId',
|
|
'chat+report': '/app/chats/$chatId/report/$reportId',
|
|
'chat+dashboard+metric': '/app/chats/$chatId/dashboards/$dashboardId/metrics/$metricId',
|
|
'chat+report+metric': '/app/chats/$chatId/report/$reportId/metrics/$metricId',
|
|
|
|
// Collection combination routes
|
|
'collection+chat': '/app/collections/$collectionId/chats/$chatId',
|
|
'collection+dashboard': '/app/collections/$collectionId/dashboard/$dashboardId',
|
|
'collection+metric': '/app/collections/$collectionId/metrics/$metricId',
|
|
'collection+chat+dashboard':
|
|
'/app/collections/$collectionId/chats/$chatId/dashboards/$dashboardId',
|
|
'collection+chat+metric': '/app/collections/$collectionId/chats/$chatId/metrics/$metricId',
|
|
'collection+dashboard+metric':
|
|
'/app/collections/$collectionId/dashboard/$dashboardId/metrics/$metricId',
|
|
'collection+chat+dashboard+metric':
|
|
'/app/collections/$collectionId/chats/$chatId/dashboards/$dashboardId/metrics/$metricId',
|
|
} as const;
|
|
|
|
// Type-check that all routes are valid
|
|
type _RouteMapCheck = typeof ROUTE_MAP extends Record<string, FileRouteTypes['to']> ? true : false;
|
|
type RouteMap = typeof ROUTE_MAP;
|
|
|
|
/**
|
|
* Helper type to get valid route keys based on builder state
|
|
*/
|
|
type GetRouteKey<T extends RouteBuilderState> =
|
|
// Collection combination routes (most specific first)
|
|
T extends { collectionId: string; chatId: string; dashboardId: string; metricId: string }
|
|
? 'collection+chat+dashboard+metric'
|
|
: T extends { collectionId: string; chatId: string; dashboardId: string }
|
|
? 'collection+chat+dashboard'
|
|
: T extends { collectionId: string; chatId: string; metricId: string }
|
|
? 'collection+chat+metric'
|
|
: T extends { collectionId: string; dashboardId: string; metricId: string }
|
|
? 'collection+dashboard+metric'
|
|
: T extends { collectionId: string; chatId: string }
|
|
? 'collection+chat'
|
|
: T extends { collectionId: string; dashboardId: string }
|
|
? 'collection+dashboard'
|
|
: T extends { collectionId: string; metricId: string }
|
|
? 'collection+metric'
|
|
: T extends { collectionId: string }
|
|
? 'collection'
|
|
: // Chat combination routes
|
|
T extends { chatId: string; dashboardId: string; metricId: string }
|
|
? 'chat+dashboard+metric'
|
|
: T extends { chatId: string; reportId: string; metricId: string }
|
|
? 'chat+report+metric'
|
|
: T extends { chatId: string; dashboardId: string }
|
|
? 'chat+dashboard'
|
|
: T extends { chatId: string; reportId: string }
|
|
? 'chat+report'
|
|
: T extends { chatId: string; metricId: string }
|
|
? 'chat+metric'
|
|
: T extends { chatId: string }
|
|
? 'chat'
|
|
: // Direct asset combination routes
|
|
T extends { dashboardId: string; metricId: string }
|
|
? 'dashboard+metric'
|
|
: T extends { reportId: string; metricId: string }
|
|
? 'report+metric'
|
|
: // Single asset routes
|
|
T extends { dashboardId: string }
|
|
? 'dashboard'
|
|
: T extends { metricId: string }
|
|
? 'metric'
|
|
: T extends { reportId: string }
|
|
? 'report'
|
|
: never;
|
|
|
|
/**
|
|
* Type-safe route builder with fluent API
|
|
*/
|
|
class RouteBuilder<T extends RouteBuilderState = NonNullable<unknown>> {
|
|
private state: T;
|
|
|
|
constructor(state: T = {} as T) {
|
|
this.state = state;
|
|
}
|
|
|
|
/**
|
|
* Add collection ID to the route
|
|
*/
|
|
withCollection<U extends string>(collectionId: U): RouteBuilder<T & { collectionId: U }> {
|
|
return new RouteBuilder({ ...this.state, collectionId });
|
|
}
|
|
|
|
/**
|
|
* Add chat ID to the route
|
|
*/
|
|
withChat<U extends string>(chatId: U): RouteBuilder<T & { chatId: U }> {
|
|
return new RouteBuilder({ ...this.state, chatId });
|
|
}
|
|
|
|
/**
|
|
* Add metric ID to the route
|
|
*/
|
|
withMetric<U extends string>(metricId: U): RouteBuilder<T & { metricId: U }> {
|
|
return new RouteBuilder({ ...this.state, metricId });
|
|
}
|
|
|
|
/**
|
|
* Add dashboard ID to the route
|
|
*/
|
|
withDashboard<U extends string>(dashboardId: U): RouteBuilder<T & { dashboardId: U }> {
|
|
return new RouteBuilder({ ...this.state, dashboardId });
|
|
}
|
|
|
|
/**
|
|
* Add report ID to the route
|
|
*/
|
|
withReport<U extends string>(reportId: U): RouteBuilder<T & { reportId: U }> {
|
|
return new RouteBuilder({ ...this.state, reportId });
|
|
}
|
|
|
|
/**
|
|
* Add version number to the route (for the primary asset)
|
|
* The search parameter name will be determined based on the primary asset type:
|
|
* - metric_version_number for metrics
|
|
* - dashboard_version_number for dashboards
|
|
* - report_version_number for reports
|
|
*/
|
|
withVersion<U extends number>(versionNumber: U): RouteBuilder<T & { versionNumber: U }> {
|
|
return new RouteBuilder({ ...this.state, versionNumber });
|
|
}
|
|
|
|
/**
|
|
* Add metric version number to the route
|
|
*/
|
|
withMetricVersion<U extends number>(
|
|
metricVersionNumber: U
|
|
): RouteBuilder<T & { metricVersionNumber: U }> {
|
|
return new RouteBuilder({ ...this.state, metricVersionNumber });
|
|
}
|
|
|
|
/**
|
|
* Add dashboard version number to the route
|
|
*/
|
|
withDashboardVersion<U extends number>(
|
|
dashboardVersionNumber: U
|
|
): RouteBuilder<T & { dashboardVersionNumber: U }> {
|
|
return new RouteBuilder({ ...this.state, dashboardVersionNumber });
|
|
}
|
|
|
|
/**
|
|
* Add report version number to the route
|
|
*/
|
|
withReportVersion<U extends number>(
|
|
reportVersionNumber: U
|
|
): RouteBuilder<T & { reportVersionNumber: U }> {
|
|
return new RouteBuilder({ ...this.state, reportVersionNumber });
|
|
}
|
|
|
|
/**
|
|
* Build the route path with type safety
|
|
*/
|
|
build(): GetRouteKey<T> extends keyof RouteMap ? RouteMap[GetRouteKey<T>] : never {
|
|
const key = this.getRouteKey();
|
|
|
|
return ROUTE_MAP[key as keyof RouteMap] as GetRouteKey<T> extends keyof RouteMap
|
|
? RouteMap[GetRouteKey<T>]
|
|
: never;
|
|
}
|
|
|
|
/**
|
|
* Get the route params object for TanStack Router navigation
|
|
*/
|
|
getParams(): T {
|
|
return this.state;
|
|
}
|
|
|
|
/**
|
|
* Build navigation options with route and params
|
|
*/
|
|
buildNavigationOptions(): OptionsTo {
|
|
const route = this.build();
|
|
const params = this.getParams();
|
|
const search = this.getSearchParams();
|
|
|
|
// Build navigation options object, only including non-empty params and search
|
|
const navOptions: Record<string, unknown> = { to: route };
|
|
|
|
// Only include params if they contain actual route parameters (excluding version numbers)
|
|
const routeParams = this.getRouteOnlyParams();
|
|
if (Object.keys(routeParams).length > 0) {
|
|
navOptions.params = routeParams;
|
|
}
|
|
|
|
// Only include search if it contains version numbers
|
|
if (Object.keys(search).length > 0) {
|
|
navOptions.search = search;
|
|
}
|
|
|
|
// Type assertion through unknown for complex generic type
|
|
return navOptions as OptionsTo;
|
|
}
|
|
|
|
/**
|
|
* Get only the route parameters (excluding version numbers)
|
|
*/
|
|
private getRouteOnlyParams(): Record<string, unknown> {
|
|
const {
|
|
versionNumber,
|
|
metricVersionNumber,
|
|
dashboardVersionNumber,
|
|
reportVersionNumber,
|
|
...routeParams
|
|
} = this.state;
|
|
return routeParams as Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* Get search parameters for version numbers
|
|
*/
|
|
private getSearchParams(): Record<string, number> {
|
|
const search: Record<string, number> = {};
|
|
const { versionNumber, metricVersionNumber, dashboardVersionNumber, reportVersionNumber } =
|
|
this.state;
|
|
|
|
// Map internal version numbers to TanStack Router search parameter names
|
|
if (versionNumber !== undefined) {
|
|
// For primary asset version, determine the correct search param name based on route structure
|
|
const { metricId, dashboardId, reportId, chatId, collectionId } = this.state;
|
|
|
|
if (dashboardId && !chatId && !collectionId) {
|
|
// Direct dashboard route or dashboard+metric route
|
|
search.dashboard_version_number = versionNumber;
|
|
} else if (reportId && !chatId && !collectionId) {
|
|
// Direct report route or report+metric route
|
|
search.report_version_number = versionNumber;
|
|
} else if (metricId && !dashboardId && !reportId) {
|
|
// Pure metric route
|
|
search.metric_version_number = versionNumber;
|
|
} else if (metricId) {
|
|
// Complex route with metric - metric is the most specific asset
|
|
search.metric_version_number = versionNumber;
|
|
} else if (dashboardId) {
|
|
// Complex route with dashboard (but no metric)
|
|
search.dashboard_version_number = versionNumber;
|
|
} else if (reportId) {
|
|
// Complex route with report (but no metric/dashboard)
|
|
search.report_version_number = versionNumber;
|
|
}
|
|
}
|
|
|
|
if (metricVersionNumber !== undefined) {
|
|
search.metric_version_number = metricVersionNumber;
|
|
}
|
|
|
|
if (dashboardVersionNumber !== undefined) {
|
|
search.dashboard_version_number = dashboardVersionNumber;
|
|
}
|
|
|
|
if (reportVersionNumber !== undefined) {
|
|
search.report_version_number = reportVersionNumber;
|
|
}
|
|
|
|
return search;
|
|
}
|
|
|
|
/**
|
|
* Determine the route key based on current state
|
|
*/
|
|
private getRouteKey(): string {
|
|
const { collectionId, chatId, dashboardId, metricId, reportId } = this.state;
|
|
|
|
// Collection combination routes (most specific first)
|
|
if (collectionId && chatId && dashboardId && metricId)
|
|
return 'collection+chat+dashboard+metric';
|
|
if (collectionId && chatId && dashboardId) return 'collection+chat+dashboard';
|
|
if (collectionId && chatId && metricId) return 'collection+chat+metric';
|
|
if (collectionId && dashboardId && metricId) return 'collection+dashboard+metric';
|
|
if (collectionId && chatId) return 'collection+chat';
|
|
if (collectionId && dashboardId) return 'collection+dashboard';
|
|
if (collectionId && metricId) return 'collection+metric';
|
|
|
|
// Chat combination routes (most specific first)
|
|
if (chatId && dashboardId && metricId) return 'chat+dashboard+metric';
|
|
if (chatId && reportId && metricId) return 'chat+report+metric';
|
|
if (chatId && dashboardId) return 'chat+dashboard';
|
|
if (chatId && reportId) return 'chat+report';
|
|
if (chatId && metricId) return 'chat+metric';
|
|
|
|
// Direct asset combination routes
|
|
if (dashboardId && metricId) return 'dashboard+metric';
|
|
if (reportId && metricId) return 'report+metric';
|
|
|
|
// Single asset routes
|
|
if (collectionId) return 'collection';
|
|
if (chatId) return 'chat';
|
|
if (dashboardId) return 'dashboard';
|
|
if (metricId) return 'metric';
|
|
if (reportId) return 'report';
|
|
|
|
throw new Error('No valid route could be determined from the provided parameters');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main function to convert asset params to route navigation options
|
|
* Returns type-safe navigation options that can be passed to Link or navigate
|
|
*
|
|
* @example
|
|
* // Navigate to a metric with version
|
|
* const options = assetParamsToRoute({
|
|
* assetType: 'metric',
|
|
* assetId: 'metric-123',
|
|
* versionNumber: 5
|
|
* });
|
|
* // Result: { to: '/app/metrics/metric-123', params: { metricId: 'metric-123' }, search: { metric_version_number: 5 } }
|
|
*
|
|
* @example
|
|
* // Navigate to standalone asset (no params or search when empty)
|
|
* const options = assetParamsToRoute({
|
|
* assetType: 'dashboard',
|
|
* assetId: 'dashboard-456'
|
|
* });
|
|
* // Result: { to: '/app/dashboards/dashboard-456', params: { dashboardId: 'dashboard-456' } }
|
|
*
|
|
* @example
|
|
* // Navigate to dashboard with metric and both versions
|
|
* const options = assetParamsToRoute({
|
|
* assetType: 'dashboard',
|
|
* assetId: 'dashboard-456',
|
|
* metricId: 'metric-789',
|
|
* versionNumber: 3,
|
|
* metricVersionNumber: 2
|
|
* });
|
|
* // Result: { to: '/app/dashboards/dashboard-456', params: { dashboardId: 'dashboard-456', metricId: 'metric-789' }, search: { dashboard_version_number: 3, metric_version_number: 2 } }
|
|
*/
|
|
export const assetParamsToRoute = (params: AssetParamsToRoute): OptionsTo => {
|
|
const builder = new RouteBuilder();
|
|
|
|
// Build route based on asset type and additional params
|
|
switch (params.assetType) {
|
|
case 'chat': {
|
|
let route = builder.withChat(params.assetId);
|
|
if (params.dashboardId) route = route.withDashboard(params.dashboardId);
|
|
if (params.reportId) route = route.withReport(params.reportId);
|
|
if (params.metricId) route = route.withMetric(params.metricId);
|
|
|
|
// Add version numbers
|
|
if (params.dashboardVersionNumber !== undefined)
|
|
route = route.withDashboardVersion(params.dashboardVersionNumber);
|
|
if (params.metricVersionNumber !== undefined)
|
|
route = route.withMetricVersion(params.metricVersionNumber);
|
|
if (params.reportVersionNumber !== undefined)
|
|
route = route.withReportVersion(params.reportVersionNumber);
|
|
|
|
return route.buildNavigationOptions();
|
|
}
|
|
|
|
case 'metric': {
|
|
let route = builder.withMetric(params.assetId);
|
|
if (params.chatId) {
|
|
route = route.withChat(params.chatId);
|
|
if (params.dashboardId) route = route.withDashboard(params.dashboardId);
|
|
if (params.reportId) route = route.withReport(params.reportId);
|
|
}
|
|
|
|
// Add version number for the metric
|
|
if (params.versionNumber !== undefined) route = route.withVersion(params.versionNumber);
|
|
|
|
return route.buildNavigationOptions();
|
|
}
|
|
|
|
case 'dashboard': {
|
|
let route = builder.withDashboard(params.assetId);
|
|
if (params.chatId) {
|
|
route = route.withChat(params.chatId);
|
|
if (params.metricId) route = route.withMetric(params.metricId);
|
|
} else if (params.metricId) {
|
|
// Direct dashboard+metric route (without chat context)
|
|
route = route.withMetric(params.metricId);
|
|
}
|
|
|
|
// Add version numbers
|
|
if (params.versionNumber !== undefined) route = route.withVersion(params.versionNumber);
|
|
if (params.metricVersionNumber !== undefined)
|
|
route = route.withMetricVersion(params.metricVersionNumber);
|
|
|
|
return route.buildNavigationOptions();
|
|
}
|
|
|
|
case 'report': {
|
|
let route = builder.withReport(params.assetId);
|
|
if (params.chatId) {
|
|
route = route.withChat(params.chatId);
|
|
if (params.metricId) route = route.withMetric(params.metricId);
|
|
} else if (params.metricId) {
|
|
// Direct report+metric route (without chat context)
|
|
route = route.withMetric(params.metricId);
|
|
}
|
|
|
|
// Add version numbers
|
|
if (params.versionNumber !== undefined) route = route.withVersion(params.versionNumber);
|
|
if (params.metricVersionNumber !== undefined)
|
|
route = route.withMetricVersion(params.metricVersionNumber);
|
|
|
|
return route.buildNavigationOptions();
|
|
}
|
|
|
|
case 'collection': {
|
|
let route = builder.withCollection(params.assetId);
|
|
if (params.chatId) route = route.withChat(params.chatId);
|
|
if (params.dashboardId) route = route.withDashboard(params.dashboardId);
|
|
if (params.metricId) route = route.withMetric(params.metricId);
|
|
|
|
// Add version numbers
|
|
if (params.metricVersionNumber !== undefined)
|
|
route = route.withMetricVersion(params.metricVersionNumber);
|
|
if (params.dashboardVersionNumber !== undefined)
|
|
route = route.withDashboardVersion(params.dashboardVersionNumber);
|
|
|
|
return route.buildNavigationOptions();
|
|
}
|
|
|
|
default:
|
|
console.warn(`Unknown asset type: ${(params as AssetParamsToRoute).assetType}`, params);
|
|
throw new Error(`Unknown asset type: ${(params as AssetParamsToRoute).assetType}`, params);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Create a new route builder instance
|
|
*/
|
|
export const createRouteBuilder = () => new RouteBuilder();
|
|
|
|
/**
|
|
* Helper function to get just the route path from asset params
|
|
* Use this when you only need the path string without params
|
|
*/
|
|
export const assetParamsToRoutePath = (params: AssetParamsToRoute): RouteFilePaths => {
|
|
const navOptions = assetParamsToRoute(params);
|
|
return navOptions.to as RouteFilePaths;
|
|
};
|