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",
|
name = "app",
|
||||||
srcs = [
|
srcs = [
|
||||||
"index.ts",
|
"index.ts",
|
||||||
|
"layout.ts",
|
||||||
|
"pages.ts",
|
||||||
|
"utils.ts",
|
||||||
],
|
],
|
||||||
allow_js = True,
|
allow_js = True,
|
||||||
resolve_json_module = True,
|
resolve_json_module = True,
|
||||||
|
|
@ -89,7 +92,10 @@ esbuild(
|
||||||
ts_project(
|
ts_project(
|
||||||
name = "test_app",
|
name = "test_app",
|
||||||
testonly = True,
|
testonly = True,
|
||||||
srcs = ["index.test.ts"],
|
srcs = [
|
||||||
|
"index.test.ts",
|
||||||
|
"utils.test.ts",
|
||||||
|
],
|
||||||
allow_js = True,
|
allow_js = True,
|
||||||
resolve_json_module = True,
|
resolve_json_module = True,
|
||||||
transpiler = "tsc",
|
transpiler = "tsc",
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link href="/dist.css" rel="stylesheet">
|
<title>DataBuild Dashboard</title>
|
||||||
<script src="/app_dist.js"></script>
|
<link href="/static/dist.css" rel="stylesheet">
|
||||||
|
<script src="/static/app_dist.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1 class="text-3xl font-bold underline"> Hello world! </h1>
|
|
||||||
<div id="app">
|
<div id="app">
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,37 @@
|
||||||
import m from 'mithril';
|
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";
|
export const appName = "databuild";
|
||||||
|
|
||||||
const ele = m("div", {class:"alert shadow-lg max-w-sm","role":"alert"},
|
// Wrapper components that include layout
|
||||||
[
|
const LayoutWrapper = (component: any) => ({
|
||||||
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"},
|
view: (vnode: any) => m(Layout, m(component, vnode.attrs))
|
||||||
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",
|
// Route definitions
|
||||||
[
|
const routes = {
|
||||||
m("h3", {"class":"font-bold"},
|
'/': LayoutWrapper(RecentActivity),
|
||||||
"Beans!"
|
'/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") {
|
if (typeof window !== "undefined") {
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
m.mount(
|
// Set up routing
|
||||||
document.getElementById('app') as HTMLFormElement,
|
m.route(document.getElementById('app') as HTMLElement, '/', routes);
|
||||||
{ view: () => [
|
});
|
||||||
ele,
|
|
||||||
] }
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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