Skip to content

Commit

Permalink
refactor(ui): use page components in test runs and profiling runs
Browse files Browse the repository at this point in the history
  • Loading branch information
aarthy-dk committed Oct 3, 2024
1 parent f8e26f3 commit dc968b5
Show file tree
Hide file tree
Showing 7 changed files with 417 additions and 196 deletions.
29 changes: 29 additions & 0 deletions testgen/ui/components/frontend/js/display_utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
function formatTimestamp(/** @type number */ timestamp) {
if (!timestamp) {
return '--';
}

const date = new Date(timestamp);
const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
const hours = date.getHours();
const minutes = date.getMinutes();
return `${months[date.getMonth()]} ${date.getDate()}, ${hours % 12}:${String(minutes).padStart(2, '0')} ${hours / 12 > 1 ? 'PM' : 'AM'}`;
}

function formatDuration(/** @type string */ duration) {
if (!duration) {
return '--';
}

const { hour, minute, second } = duration.split(':');
let formatted = [
{ value: Number(hour), unit: 'h' },
{ value: Number(minute), unit: 'm' },
{ value: Number(second), unit: 's' },
].map(({ value, unit }) => value ? `${value}${unit}` : '')
.join(' ');

return formatted.trim() || '< 1s';
}

export { formatTimestamp, formatDuration };
4 changes: 4 additions & 0 deletions testgen/ui/components/frontend/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { Link } from './components/link.js';
import { Paginator } from './components/paginator.js';
import { Select } from './components/select.js'
import { SortingSelector } from './components/sorting_selector.js';
import { TestRuns } from './pages/test_runs.js';
import { ProfilingRuns } from './pages/profiling_runs.js';

let currentWindowVan = van;
let topWindowVan = window.top.van;
Expand All @@ -28,6 +30,8 @@ const TestGenComponent = (/** @type {string} */ id, /** @type {object} */ props)
select: Select,
sorting_selector: SortingSelector,
sidebar: window.top.testgen.components.Sidebar,
test_runs: TestRuns,
profiling_runs: ProfilingRuns,
};

if (Object.keys(componentById).includes(id)) {
Expand Down
160 changes: 160 additions & 0 deletions testgen/ui/components/frontend/js/pages/profiling_runs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* @typedef Properties
* @type {object}
* @property {array} items
*/
import van from '../van.min.js';
import { Tooltip } from '../van-tooltip.js';
import { SummaryBar } from '../components/summary_bar.js';
import { Link } from '../components/link.js';
import { Button } from '../components/button.js';
import { Streamlit } from '../streamlit.js';
import { wrapProps } from '../utils.js';
import { formatTimestamp, formatDuration } from '../display_utils.js';

const { div, span, i } = van.tags;

const ProfilingRuns = (/** @type Properties */ props) => {
window.testgen.isPage = true;

const profilingRunItems = van.derive(() => {
let items = [];
try {
items = JSON.parse(props.items?.val);
} catch { }
Streamlit.setFrameHeight(44 + 84.5 * items.length);
return items;
});
const columns = ['20%', '20%', '20%', '40%'];

return div(
() => div(
{ class: 'table' },
div(
{ class: 'table-header flex-row' },
span(
{ style: `flex: ${columns[0]}` },
'Start Time | Table Group',
),
span(
{ style: `flex: ${columns[1]}` },
'Status | Duration',
),
span(
{ style: `flex: ${columns[2]}` },
'Schema',
),
span(
{ style: `flex: ${columns[3]}` },
'Hygiene Issues',
),
),
profilingRunItems.val.map(item => ProfilingRunItem(item, columns)),
),
);
}

const ProfilingRunItem = (item, /** @type string[] */ columns) => {
return div(
{ class: 'table-row flex-row' },
div(
{ style: `flex: ${columns[0]}` },
div(formatTimestamp(item.start_time)),
div(
{ class: 'text-caption mt-1' },
item.table_groups_name,
),
),
div(
{ class: 'flex-row', style: `flex: ${columns[1]}` },
div(
ProfilingRunStatus(item),
div(
{ class: 'text-caption mt-1' },
formatDuration(item.duration),
),
),
item.status === 'Running' && item.process_id ? Button(wrapProps({
type: 'stroked',
label: 'Cancel Run',
style: 'width: auto; height: 32px; color: var(--purple); margin-left: 16px;',
onclick: () => Streamlit.sendData({
event: 'RunCanceled',
profiling_run: item,
_id: Math.random(), // Forces on_change component handler to be triggered on every click
}),
})) : null,
),
div(
{ style: `flex: ${columns[2]}` },
div(item.schema_name),
div(
{
class: 'text-caption mt-1 mb-1',
style: item.status === 'Complete' && !item.column_ct ? 'color: var(--red);' : '',
},
`${item.table_ct || 0} tables, ${item.column_ct || 0} columns`,
),
item.column_ct ? Link(wrapProps({
label: 'View results',
href: 'profiling-runs:results',
params: { 'run_id': item.profiling_run_id },
underline: true,
right_icon: 'chevron_right',
})) : null,
),
div(
{ style: `flex: ${columns[3]}` },
item.anomaly_ct ? SummaryBar(wrapProps({
items: [
{ label: 'Definite', value: item.anomalies_definite_ct, color: 'red' },
{ label: 'Likely', value: item.anomalies_likely_ct, color: 'orange' },
{ label: 'Possible', value: item.anomalies_possible_ct, color: 'yellow' },
{ label: 'Dismissed', value: item.anomalies_dismissed_ct, color: 'grey' },
],
height: 10,
width: 300,
})) : '--',
item.anomaly_ct ? Link(wrapProps({
label: `View ${item.anomaly_ct} issues`,
href: 'profiling-runs:hygiene',
params: { 'run_id': item.profiling_run_id },
underline: true,
right_icon: 'chevron_right',
style: 'margin-top: 8px;',
})) : null,
),
);
}

function ProfilingRunStatus(/** @type object */ item) {
const attributeMap = {
Running: { label: 'Running', color: 'blue' },
Complete: { label: 'Completed', color: '' },
Error: { label: 'Error', color: 'red' },
Cancelled: { label: 'Canceled', color: 'purple' },
};
const attributes = attributeMap[item.status] || { label: 'Unknown', color: 'grey' };
return span(
{
class: 'flex-row',
style: `color: var(--${attributes.color});`,
},
attributes.label,
() => {
const tooltipError = van.state(false);
return item.status === 'Error' && item.log_message ? i(
{
class: 'material-symbols-rounded text-secondary ml-1 profiling-runs--info',
style: 'position: relative; font-size: 16px;',
onmouseenter: () => tooltipError.val = true,
onmouseleave: () => tooltipError.val = false,
},
'info',
Tooltip({ text: item.log_message, show: tooltipError }),
) : null;
},
);
}

export { ProfilingRuns };
136 changes: 136 additions & 0 deletions testgen/ui/components/frontend/js/pages/test_runs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* @typedef Properties
* @type {object}
* @property {array} items
*/
import van from '../van.min.js';
import { Tooltip } from '../van-tooltip.js';
import { SummaryBar } from '../components/summary_bar.js';
import { Link } from '../components/link.js';
import { Button } from '../components/button.js';
import { Streamlit } from '../streamlit.js';
import { wrapProps } from '../utils.js';
import { formatTimestamp, formatDuration } from '../display_utils.js';

const { div, span, i } = van.tags;

const TestRuns = (/** @type Properties */ props) => {
window.testgen.isPage = true;

const testRunItems = van.derive(() => {
let items = [];
try {
items = JSON.parse(props.items?.val);
} catch { }
Streamlit.setFrameHeight(44 + 60.5 * items.length);
return items;
});
const columns = ['30%', '20%', '50%'];

return div(
() => div(
{ class: 'table' },
div(
{ class: 'table-header flex-row' },
span(
{ style: `flex: ${columns[0]}` },
'Start Time | Table Group | Test Suite',
),
span(
{ style: `flex: ${columns[1]}` },
'Status | Duration',
),
span(
{ style: `flex: ${columns[2]}` },
'Results Summary',
),
),
testRunItems.val.map(item => TestRunItem(item, columns)),
),
);
}

const TestRunItem = (item, /** @type string[] */ columns) => {
return div(
{ class: 'table-row flex-row' },
div(
{ style: `flex: ${columns[0]}` },
Link(wrapProps({
label: formatTimestamp(item.test_starttime),
href: 'test-runs:results',
params: { 'run_id': item.test_run_id },
underline: true,
})),
div(
{ class: 'text-caption mt-1' },
`${item.table_groups_name} > ${item.test_suite}`,
),
),
div(
{ class: 'flex-row', style: `flex: ${columns[1]}` },
div(
TestRunStatus(item),
div(
{ class: 'text-caption mt-1' },
formatDuration(item.duration),
),
),
item.status === 'Running' && item.process_id ? Button(wrapProps({
type: 'stroked',
label: 'Cancel Run',
style: 'width: auto; height: 32px; color: var(--purple); margin-left: 16px;',
onclick: () => Streamlit.sendData({
event: 'RunCanceled',
test_run: item,
_id: Math.random(), // Forces on_change component handler to be triggered on every click
}),
})) : null,
),
div(
{ style: `flex: ${columns[2]}` },
item.test_ct ? SummaryBar(wrapProps({
items: [
{ label: 'Passed', value: item.passed_ct, color: 'green' },
{ label: 'Warning', value: item.warning_ct, color: 'yellow' },
{ label: 'Failed', value: item.failed_ct, color: 'red' },
{ label: 'Error', value: item.error_ct, color: 'brown' },
{ label: 'Dismissed', value: item.dismissed_ct, color: 'grey' },
],
height: 10,
width: 300,
})) : '--',
),
);
}

function TestRunStatus(/** @type object */ item) {
const attributeMap = {
Running: { label: 'Running', color: 'blue' },
Complete: { label: 'Completed', color: '' },
Error: { label: 'Error', color: 'red' },
Cancelled: { label: 'Canceled', color: 'purple' },
};
const attributes = attributeMap[item.status] || { label: 'Unknown', color: 'grey' };
return span(
{
class: 'flex-row',
style: `color: var(--${attributes.color});`,
},
attributes.label,
() => {
const tooltipError = van.state(false);
return item.status === 'Error' && item.log_message ? i(
{
class: 'material-symbols-rounded text-secondary ml-1',
style: 'position: relative; font-size: 16px;',
onmouseenter: () => tooltipError.val = true,
onmouseleave: () => tooltipError.val = false,
},
'info',
Tooltip({ text: item.log_message, show: tooltipError }),
) : null;
},
);
}

export { TestRuns };
52 changes: 52 additions & 0 deletions testgen/ui/components/frontend/js/van-tooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Code modified from vanjs-ui
// https://www.npmjs.com/package/vanjs-ui
// https://cdn.jsdelivr.net/npm/[email protected]/dist/van-ui.nomodule.js

import van from './van.min.js';
const { div, span } = van.tags;

const toStyleStr = (style) => Object.entries(style).map(([k, v]) => `${k}: ${v};`).join("");

const Tooltip = ({ text, show, backgroundColor = '#333D', fontColor = 'white', fadeInSec = 0.3, tooltipClass = '', tooltipStyleOverrides = {}, triangleClass = '', triangleStyleOverrides = {}, }) => {
const tooltipStylesStr = toStyleStr({
width: 'max-content',
'min-width': '100px',
'max-width': '400px',
visibility: 'hidden',
'background-color': backgroundColor,
color: fontColor,
'text-align': 'center',
padding: '5px',
'border-radius': '5px',
position: 'absolute',
'z-index': 1,
bottom: '125%',
left: '50%',
transform: 'translateX(-50%)',
opacity: 0,
transition: `opacity ${fadeInSec}s`,
'font-size': '14px',
'font-family': `'Roboto', 'Helvetica Neue', sans-serif`,
'text-wrap': 'wrap',
...tooltipStyleOverrides,
});
const triangleStylesStr = toStyleStr({
width: 0,
height: 0,
'margin-left': '-5px',
'border-left': '5px solid transparent',
'border-right': '5px solid transparent',
'border-top': '5px solid #333',
position: 'absolute',
bottom: '-5px',
left: '50%',
...triangleStyleOverrides,
});
const dom = span({ class: tooltipClass, style: tooltipStylesStr }, text, div({ class: triangleClass, style: triangleStylesStr }));
van.derive(() => show.val ?
(dom.style.opacity = '1', dom.style.visibility = 'visible') :
(dom.style.opacity = '0', dom.style.visibility = 'hidden'));
return dom;
};

export { Tooltip };
Loading

0 comments on commit dc968b5

Please sign in to comment.