Implement basic app and routing
This commit is contained in:
parent
57a0238f09
commit
98583eb7ba
8 changed files with 492 additions and 30 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
35
databuild/dashboard/layout.ts
Normal file
35
databuild/dashboard/layout.ts
Normal 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),
|
||||
]
|
||||
};
|
||||
143
databuild/dashboard/pages.ts
Normal file
143
databuild/dashboard/pages.ts
Normal 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'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
};
|
||||
52
databuild/dashboard/utils.test.ts
Normal file
52
databuild/dashboard/utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
11
databuild/dashboard/utils.ts
Normal file
11
databuild/dashboard/utils.ts
Normal 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
Loading…
Reference in a new issue