Implement basic app and routing

This commit is contained in:
soaxelbrooke 2025-07-12 08:43:44 -07:00
parent 57a0238f09
commit 98583eb7ba
8 changed files with 492 additions and 30 deletions

View file

@ -61,6 +61,9 @@ ts_project(
name = "app",
srcs = [
"index.ts",
"layout.ts",
"pages.ts",
"utils.ts",
],
allow_js = True,
resolve_json_module = True,
@ -89,7 +92,10 @@ esbuild(
ts_project(
name = "test_app",
testonly = True,
srcs = ["index.test.ts"],
srcs = [
"index.test.ts",
"utils.test.ts",
],
allow_js = True,
resolve_json_module = True,
transpiler = "tsc",

View file

@ -3,11 +3,11 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/dist.css" rel="stylesheet">
<script src="/app_dist.js"></script>
<title>DataBuild Dashboard</title>
<link href="/static/dist.css" rel="stylesheet">
<script src="/static/app_dist.js"></script>
</head>
<body>
<h1 class="text-3xl font-bold underline"> Hello world! </h1>
<div id="app">
Loading...
</div>

View file

@ -1,30 +1,37 @@
import m from 'mithril';
// import * as foo from './typescript_client/apis/DefaultApi.ts';
import { Layout } from './layout';
import {
RecentActivity,
BuildStatus,
PartitionsList,
PartitionStatus,
JobsList,
JobMetrics,
GraphAnalysis
} from './pages';
import { decodePartitionRef } from './utils';
export const appName = "databuild";
const ele = m("div", {class:"alert shadow-lg max-w-sm","role":"alert"},
[
m("svg", {class:"stroke-info h-6 w-6 shrink-0","xmlns":"http://www.w3.org/2000/svg","fill":"none","viewBox":"0 0 24 24"},
m("path", {"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2","d":"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"})
),
m("div",
[
m("h3", {"class":"font-bold"},
"Beans!"
)
]
)
]
);
// Wrapper components that include layout
const LayoutWrapper = (component: any) => ({
view: (vnode: any) => m(Layout, m(component, vnode.attrs))
});
// Route definitions
const routes = {
'/': LayoutWrapper(RecentActivity),
'/builds/:id': LayoutWrapper(BuildStatus),
'/partitions': LayoutWrapper(PartitionsList),
'/partitions/:base64_ref': LayoutWrapper(PartitionStatus),
'/jobs': LayoutWrapper(JobsList),
'/jobs/:label': LayoutWrapper(JobMetrics),
'/analyze': LayoutWrapper(GraphAnalysis),
};
if (typeof window !== "undefined") {
document.addEventListener("DOMContentLoaded", () => {
m.mount(
document.getElementById('app') as HTMLFormElement,
{ view: () => [
ele,
] }
);
})
// Set up routing
m.route(document.getElementById('app') as HTMLElement, '/', routes);
});
}

View file

@ -0,0 +1,35 @@
import m from 'mithril';
export const Layout = {
view: (vnode: any) => [
m('header.navbar.bg-base-100.shadow-lg', [
m('div.navbar-start', [
m('div.dropdown', [
m('div.btn.btn-ghost.lg:hidden[tabindex="0"][role="button"]', [
m('svg.w-5.h-5[xmlns="http://www.w3.org/2000/svg"][fill="none"][viewBox="0 0 24 24"]', [
m('path[stroke-linecap="round"][stroke-linejoin="round"][stroke-width="2"][stroke="currentColor"][d="M4 6h16M4 12h8m-8 6h16"]'),
]),
]),
m('ul.menu.menu-sm.dropdown-content.bg-base-100.rounded-box.z-1.mt-3.w-52.p-2.shadow[tabindex="0"]', [
m('li', m(m.route.Link, { href: '/partitions' }, 'Partitions')),
m('li', m(m.route.Link, { href: '/jobs' }, 'Jobs')),
m('li', m(m.route.Link, { href: '/analyze' }, 'Analyze')),
]),
]),
m(m.route.Link, { href: '/', class: 'btn btn-ghost text-xl' }, 'DataBuild Dashboard'),
]),
m('div.navbar-center.hidden.lg:flex', [
m('ul.menu.menu-horizontal.px-1', [
m('li', m(m.route.Link, { href: '/' }, 'Dashboard')),
m('li', m(m.route.Link, { href: '/partitions' }, 'Partitions')),
m('li', m(m.route.Link, { href: '/jobs' }, 'Jobs')),
m('li', m(m.route.Link, { href: '/analyze' }, 'Analyze')),
]),
]),
m('div.navbar-end', [
m('div.badge.badge-outline', 'v1.0'),
]),
]),
m('main.min-h-screen.bg-base-200.pt-4', vnode.children),
]
};

View file

@ -0,0 +1,143 @@
import m from 'mithril';
// Page scaffold components
export const RecentActivity = {
view: () => m('div.container.mx-auto.p-4', [
m('h1.text-3xl.font-bold.mb-4', 'Recent Activity'),
m('div.card.bg-base-100.shadow-xl', [
m('div.card-body', [
m('h2.card-title', 'Dashboard Home'),
m('p', 'Recent build requests and system activity will be displayed here.'),
m('div.stats.shadow', [
m('div.stat', [
m('div.stat-title', 'Active Builds'),
m('div.stat-value', '0'),
]),
m('div.stat', [
m('div.stat-title', 'Recent Partitions'),
m('div.stat-value', '0'),
]),
]),
]),
]),
])
};
export const BuildStatus = {
view: (vnode: any) => {
const buildId = vnode.attrs.id;
return m('div.container.mx-auto.p-4', [
m('h1.text-3xl.font-bold.mb-4', `Build Status: ${buildId}`),
m('div.card.bg-base-100.shadow-xl', [
m('div.card-body', [
m('h2.card-title', 'Build Request Details'),
m('p', 'Real-time build progress and job execution timeline will be displayed here.'),
m('div.alert.alert-info', [
m('span', `Monitoring build request: ${buildId}`),
]),
]),
]),
]);
}
};
export const PartitionsList = {
view: () => m('div.container.mx-auto.p-4', [
m('h1.text-3xl.font-bold.mb-4', 'Partitions'),
m('div.card.bg-base-100.shadow-xl', [
m('div.card-body', [
m('h2.card-title', 'Partition Listing'),
m('p', 'Searchable list of recently built partitions with build triggers.'),
m('div.form-control.mb-4', [
m('input.input.input-bordered[placeholder="Search partitions..."]'),
]),
m('div.alert.alert-info', [
m('span', 'Partition list will be populated from the service API.'),
]),
]),
]),
])
};
export const PartitionStatus = {
view: (vnode: any) => {
const encodedRef = vnode.attrs.base64_ref;
return m('div.container.mx-auto.p-4', [
m('h1.text-3xl.font-bold.mb-4', 'Partition Status'),
m('div.card.bg-base-100.shadow-xl', [
m('div.card-body', [
m('h2.card-title', 'Partition Details'),
m('p', 'Partition lifecycle, build history, and related information.'),
m('div.alert.alert-info', [
m('span', `Encoded reference: ${encodedRef}`),
]),
m('div.card-actions.justify-end', [
m('button.btn.btn-primary', 'Build Now'),
m('button.btn.btn-secondary', 'Force Rebuild'),
]),
]),
]),
]);
}
};
export const JobsList = {
view: () => m('div.container.mx-auto.p-4', [
m('h1.text-3xl.font-bold.mb-4', 'Jobs'),
m('div.card.bg-base-100.shadow-xl', [
m('div.card-body', [
m('h2.card-title', 'Job Listing'),
m('p', 'Jobs in the graph with high-level metadata and performance metrics.'),
m('div.alert.alert-info', [
m('span', 'Job list will be populated from the graph configuration.'),
]),
]),
]),
])
};
export const JobMetrics = {
view: (vnode: any) => {
const jobLabel = vnode.attrs.label;
return m('div.container.mx-auto.p-4', [
m('h1.text-3xl.font-bold.mb-4', `Job Metrics: ${jobLabel}`),
m('div.card.bg-base-100.shadow-xl', [
m('div.card-body', [
m('h2.card-title', 'Performance and Reliability'),
m('p', 'Success rate charts, duration trends, and recent runs.'),
m('div.stats.shadow', [
m('div.stat', [
m('div.stat-title', 'Success Rate'),
m('div.stat-value', '95%'),
]),
m('div.stat', [
m('div.stat-title', 'Avg Duration'),
m('div.stat-value', '2.5s'),
]),
]),
]),
]),
]);
}
};
export const GraphAnalysis = {
view: () => m('div.container.mx-auto.p-4', [
m('h1.text-3xl.font-bold.mb-4', 'Graph Analysis'),
m('div.card.bg-base-100.shadow-xl', [
m('div.card-body', [
m('h2.card-title', 'Interactive Build Graph'),
m('p', 'Analyze partition dependencies and execution plans.'),
m('div.form-control.mb-4', [
m('label.label', [
m('span.label-text', 'Partition References'),
]),
m('textarea.textarea.textarea-bordered[placeholder="Enter partition references to analyze..."]'),
]),
m('div.card-actions.justify-end', [
m('button.btn.btn-primary', 'Analyze Graph'),
]),
]),
]),
])
};

View file

@ -0,0 +1,52 @@
import o from 'ospec';
// Inline the utils functions for testing since we can't import from the app module in tests
function encodePartitionRef(ref: string): string {
return btoa(ref).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function decodePartitionRef(encoded: string): string {
// Add padding if needed
const padding = '='.repeat((4 - (encoded.length % 4)) % 4);
const padded = encoded.replace(/-/g, '+').replace(/_/g, '/') + padding;
return atob(padded);
}
o.spec('URL Encoding Utils', () => {
o('should encode and decode partition references correctly', () => {
const testCases = [
'simple/partition',
'complex/partition/with/slashes',
'partition+with+plus',
'partition=with=equals',
'partition_with_underscores',
'partition-with-dashes',
'partition/with/mixed+symbols=test_case-123',
];
testCases.forEach(original => {
const encoded = encodePartitionRef(original);
const decoded = decodePartitionRef(encoded);
o(decoded).equals(original)(`Failed for: ${original}`);
// Encoded string should be URL-safe (no +, /, or = characters)
o(encoded.includes('+')).equals(false)(`Encoded string contains +: ${encoded}`);
o(encoded.includes('/')).equals(false)(`Encoded string contains /: ${encoded}`);
o(encoded.includes('=')).equals(false)(`Encoded string contains =: ${encoded}`);
});
});
o('should handle empty string', () => {
const encoded = encodePartitionRef('');
const decoded = decodePartitionRef(encoded);
o(decoded).equals('');
});
o('should handle special characters', () => {
const special = 'test/path?query=value&other=123#fragment';
const encoded = encodePartitionRef(special);
const decoded = decodePartitionRef(encoded);
o(decoded).equals(special);
});
});

View file

@ -0,0 +1,11 @@
// URL encoding utilities for partition references
export function encodePartitionRef(ref: string): string {
return btoa(ref).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
export function decodePartitionRef(encoded: string): string {
// Add padding if needed
const padding = '='.repeat((4 - (encoded.length % 4)) % 4);
const padded = encoded.replace(/-/g, '+').replace(/_/g, '/') + padding;
return atob(padded);
}

File diff suppressed because one or more lines are too long