refactor: Convert Task Manager to use Components

This currently behaves the same as it did before: It still recreates the
table contents every half a second. It should also look identical,
though it's possible I missed some small differences.

The component structure is:

TaskManagerTable
- Table
  - TaskManagerRow
  - TaskManagerRow
  - TaskManagerRow
  - ...

TaskManagerRow is implemented so that we can later move to modifying
them in place as the process tree changes, instead of having to replace
them all.

Otherwise, most of the code is just moved around, and not changed much.
This commit is contained in:
Sam Atkins 2024-05-10 13:52:42 +01:00
parent e304f6fc3a
commit cf605c8a38
2 changed files with 292 additions and 324 deletions

View File

@ -2,11 +2,291 @@ import { END_HARD, END_SOFT } from "../definitions.js";
import UIAlert from "./UIAlert.js"; import UIAlert from "./UIAlert.js";
import UIContextMenu from "./UIContextMenu.js"; import UIContextMenu from "./UIContextMenu.js";
import UIWindow from "./UIWindow.js"; import UIWindow from "./UIWindow.js";
import { Component, defineComponent } from '../util/Component.js';
import UIComponentWindow from './UIComponentWindow.js';
import Table from './Components/Table.js';
import Placeholder from '../util/Placeholder.js';
import TestView from './Components/TestView.js';
const end_process = async (uuid, force) => {
const svc_process = globalThis.services.get('process');
const process = svc_process.get_by_uuid(uuid);
if (!process) {
console.warn(`Can't end process with uuid='${uuid}': does not exist`);
return;
}
let confirmation;
if ( process.is_init() ) {
if ( ! force ) {
confirmation = i18n('close_all_windows_confirm');
} else {
confirmation = i18n('restart_puter_confirm');
}
} else if ( force ) {
confirmation = i18n('end_process_force_confirm');
}
if ( confirmation ) {
const alert_resp = await UIAlert({
message: confirmation,
buttons:[
{
label: i18n('yes'),
value: true,
type: 'primary',
},
{
label: i18n('no'),
value: false,
},
]
})
if ( ! alert_resp ) return;
}
process.signal(force ? END_HARD : END_SOFT);
};
class TaskManagerTable extends Component {
static PROPERTIES = {
tasks: { value: [] },
};
static CSS = /*css*/`
:host {
flex-grow: 1;
display: flex;
flex-direction: column;
background-color: rgba(255,255,255,0.8);
border: 2px inset rgba(127, 127, 127, 0.3);
overflow: auto;
}
`;
#svc_process = globalThis.services.get('process');
create_template ({ template }) {
$(template).html(`
<div class="taskmgr-taskarea"></div>
`);
}
on_ready ({ listen }) {
this.table = new Table({
headings: [
i18n('taskmgr_header_name'),
i18n('taskmgr_header_type'),
i18n('taskmgr_header_status'),
]
});
this.table.attach(this.dom_.querySelector('.taskmgr-taskarea'));
listen('tasks', tasks => {
// TODO: Update DOM instead of replacing the entire table
this.table.set('rows', this.#iter_tasks(tasks, { indent_level: 0, is_last_item_stack: [] }));
});
}
#calculate_indent_string (indent_level, is_last_item_stack, is_last_item) {
// Returns a string of '| ├└'
let result = '';
for ( let i=0; i < indent_level; i++ ) {
const last_cell = i === indent_level - 1;
const has_trunk = (last_cell && ( ! is_last_item )) ||
(!last_cell && !is_last_item_stack[i+1]);
const has_branch = last_cell;
if (has_trunk && has_branch) {
result += '├';
} else if (has_trunk) {
result += '|';
} else if (has_branch) {
result += '└';
} else {
result += ' ';
}
}
return result;
}
#iter_tasks (items, { indent_level, is_last_item_stack }) {
const rows = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const is_last_item = i === items.length - 1;
rows.push(new TaskManagerRow({
name: item.name,
uuid: item.uuid,
process_type: item.type,
process_status: item.status.i18n_key,
indentation: this.#calculate_indent_string(indent_level, is_last_item_stack, is_last_item),
}));
const children = this.#svc_process.get_children_of(item.uuid);
if (children) {
rows.push(...this.#iter_tasks(children, {
indent_level: indent_level + 1,
is_last_item_stack:
[ ...is_last_item_stack, is_last_item ],
}));
}
}
return rows;
};
}
defineComponent('c-task-manager-table', TaskManagerTable);
class TaskManagerRow extends Component {
static PROPERTIES = {
name: {},
uuid: {},
process_type: {},
process_status: {},
indentation: { value: '' },
};
static CSS = /*css*/`
:host {
display: table-row;
}
td > span {
padding: 0 calc(2.5 * var(--scale));
}
.task {
display: flex;
height: calc(10 * var(--scale));
line-height: calc(10 * var(--scale));
}
.task-name {
flex-grow: 1;
padding-left: calc(2.5 * var(--scale));
}
.task-indentation {
display: flex;
}
.indentcell {
position: relative;
align-items: right;
width: calc(10 * var(--scale));
height: calc(10 * var(--scale));
}
.indentcell-trunk {
position: absolute;
top: 0;
left: calc(5 * var(--scale));
width: calc(5 * var(--scale));
height: calc(10 * var(--scale));
border-left: 2px solid var(--line-color);
}
.indentcell-branch {
position: absolute;
top: 0;
left: calc(5 * var(--scale));
width: calc(5 * var(--scale));
height: calc(5 * var(--scale));
border-left: 2px solid var(--line-color);
border-bottom: 2px solid var(--line-color);
border-radius: 0 0 0 calc(2.5 * var(--scale));
}
`;
create_template ({ template }) {
template.innerHTML = `
<td>
<div class="task">
<div class="task-indentation"></div>
<div class="task-name"></div>
</div>
</td>
<td><span class="process-type"></span></td>
<td><span class="process-status"></span></td>
`;
}
on_ready ({ listen }) {
listen('name', name => {
$(this.dom_).find('.task-name').text(name);
});
listen('uuid', uuid => {
this.setAttribute('data-uuid', uuid);
});
listen('process_type', type => {
$(this.dom_).find('.process-type').text(i18n('process_type_' + type));
});
listen('process_status', status => {
$(this.dom_).find('.process-status').text(i18n('process_status_' + status));
});
listen('indentation', indentation => {
const el = $(this.dom_).find('.task-indentation');
let h = '';
for (const c of indentation) {
h += `<div class="indentcell">`;
switch (c) {
case ' ':
break;
case '|':
h += `<div class="indentcell-trunk"></div>`;
break;
case '└':
h += `<div class="indentcell-branch"></div>`;
break;
case '├':
h += `<div class="indentcell-trunk"></div>`;
h += `<div class="indentcell-branch"></div>`;
break;
}
h += `</div>`;
}
el.html(h);
});
$(this).on('contextmenu', () => {
const uuid = this.get('uuid');
UIContextMenu({
items: [
{
html: i18n('close'),
onClick: () => {
end_process(uuid);
},
},
{
html: i18n('force_quit'),
onClick: () => {
end_process(uuid, true);
},
},
],
});
});
}
}
defineComponent('c-task-manager-row', TaskManagerRow);
const UIWindowTaskManager = async function UIWindowTaskManager () { const UIWindowTaskManager = async function UIWindowTaskManager () {
const svc_process = globalThis.services.get('process'); const svc_process = globalThis.services.get('process');
const w = await UIWindow({ let task_manager_table = new TaskManagerTable({
tasks: [svc_process.get_init()],
});
const interval = setInterval(() => {
const processes = [svc_process.get_init()];
task_manager_table.set('tasks', processes);
}, 500);
const w = await UIComponentWindow({
component: task_manager_table,
title: i18n('task_manager'), title: i18n('task_manager'),
icon: globalThis.icons['cog.svg'], icon: globalThis.icons['cog.svg'],
uid: null, uid: null,
@ -40,220 +320,18 @@ const UIWindowTaskManager = async function UIWindowTaskManager () {
var(--primary-lightness), var(--primary-lightness),
var(--primary-alpha))`, var(--primary-alpha))`,
'backdrop-filter': 'blur(3px)', 'backdrop-filter': 'blur(3px)',
'box-sizing': 'border-box',
} // could have been avoided with box-sizing: border-box
height: 'calc(100% - 30px)',
display: 'flex',
'flex-direction': 'column',
'--scale': '2pt',
'--line-color': '#6e6e6ebd',
},
on_close: () => {
clearInterval(interval);
},
}); });
const w_body = w.querySelector('.window-body');
w_body.classList.add('taskmgr');
const Indent = ({ has_trunk, has_branch }) => {
const el = document.createElement('div');
el.classList.add('taskmgr-indentcell');
if ( has_trunk ) {
// Add new child element
const el_indentcell_child = document.createElement('div');
el_indentcell_child.classList.add('taskmgr-indentcell-trunk');
el.appendChild(el_indentcell_child);
}
if ( has_branch ) {
const el_indentcell_child = document.createElement('div');
el_indentcell_child.classList.add('taskmgr-indentcell-branch');
el.appendChild(el_indentcell_child);
}
return {
appendTo (parent) {
parent.appendChild(el);
return this;
}
};
};
const Task = ({ placement, name }) => {
const {
indent_level, last_item,
parent_last_item,
} = placement;
const el = document.createElement('div');
el.classList.add('taskmgr-task');
for ( let i=0; i < indent_level; i++ ) {
const last_cell = i === indent_level - 1;
Indent({
has_trunk: (last_cell && ( ! last_item )) ||
(!last_cell && !parent_last_item[i+1]),
has_branch: last_cell
}).appendTo(el);
}
const el_title = document.createElement('div');
el_title.classList.add('taskmgr-task-title');
el_title.innerText = name;
el.appendChild(el_title);
return {
el () { return el; },
appendTo (parent) {
parent.appendChild(el);
return this;
}
};
}
// https://codepen.io/fomkin/pen/gOgoBVy
const Table = ({ headings }) => {
const el_table = $(`
<table>
<thead>
<tr>
${headings.map(heading =>
`<th><span>${heading}<span></th>`).join('')}
</tr>
</thead>
<tbody></tbody>
</table>
`)[0];
const el_tbody = el_table.querySelector('tbody');
return {
el () { return el_table; },
add (el) {
if ( typeof el.el === 'function' ) el = el.el();
el_tbody.appendChild(el);
return this;
},
clear () {
el_tbody.innerHTML = '';
}
};
};
const Row = () => {
const el_tr = document.createElement('tr');
return {
attach (parent) {
parent.appendChild(el_tr);
return this;
},
el () { return el_tr; },
add (el) {
if ( typeof el.el === 'function' ) el = el.el();
const el_td = document.createElement('td');
el_td.appendChild(el);
el_tr.appendChild(el_td);
return this;
}
};
};
const el_taskarea = document.createElement('div');
el_taskarea.classList.add('taskmgr-taskarea');
const tasktable = Table({
headings: [
i18n('taskmgr_header_name'),
i18n('taskmgr_header_type'),
i18n('taskmgr_header_status'),
]
});
el_taskarea.appendChild(tasktable.el());
const end_process_ = async (process, force) => {
let confirmation;
if ( process.is_init() ) {
if ( ! force ) {
confirmation = i18n('close_all_windows_confirm');
} else {
confirmation = i18n('restart_puter_confirm');
}
} else if ( force ) {
confirmation = i18n('end_process_force_confirm');
}
if ( confirmation ) {
const alert_resp = await UIAlert({
message: confirmation,
buttons:[
{
label: i18n('yes'),
value: true,
type: 'primary',
},
{
label: i18n('no'),
value: false,
},
]
})
if ( ! alert_resp ) return;
}
process.signal(force ? END_HARD : END_SOFT);
}
const iter_tasks = (items, { indent_level, parent_last_item }) => {
for ( let i=0 ; i < items.length; i++ ) {
const row = Row();
const item = items[i];
const last_item = i === items.length - 1;
row.add(Task({
placement: {
parent_last_item,
indent_level,
last_item,
},
name: item.name
}));
row.add($(`<span>${i18n('process_type_' + item.type)}</span>`)[0])
row.add($(`<span>${i18n('process_status_' + item.status.i18n_key)}</span>`)[0])
tasktable.add(row);
$(row.el()).on('contextmenu', () => {
UIContextMenu({
parent_element: $(el_taskarea),
items: [
{
html: i18n('close'),
onClick: () => {
end_process_(item);
}
},
{
html: i18n('force_quit'),
onClick: () => {
end_process_(item, true);
}
}
]
});
})
const children = svc_process.get_children_of(item.uuid);
if ( children ) {
iter_tasks(children, {
indent_level: indent_level + 1,
parent_last_item:
[...parent_last_item, last_item],
});
}
}
};
const interval = setInterval(() => {
tasktable.clear();
const processes = [svc_process.get_init()];
iter_tasks(processes, { indent_level: 0, parent_last_item: [] });
}, 500)
w.on_close = () => {
clearInterval(interval);
}
w_body.appendChild(el_taskarea);
} }
export default UIWindowTaskManager; export default UIWindowTaskManager;

View File

@ -3830,113 +3830,3 @@ fieldset[name=number-code] {
gap: 10px; gap: 10px;
justify-content: flex-end; justify-content: flex-end;
} }
.taskmgr {
box-sizing: border-box;
/* could have been avoided with box-sizing: border-box */
height: calc(100% - 30px);
display: flex;
flex-direction: column;
--scale: 2pt;
--line-color: #6e6e6ebd;
}
.taskmgr * {
box-sizing: border-box;
}
.taskmgr table {
border-collapse: collapse;
}
.taskmgr-taskarea {
flex-grow: 1;
display: flex;
flex-direction: column;
background-color: rgba(255,255,255,0.8);
border: 2px inset rgba(127, 127, 127, 0.3);
overflow: auto;
}
.taskmgr-taskarea table thead {
}
.taskmgr-taskarea table th {
-webkit-box-shadow: 0 1px 4px -2px rgba(0,0,0,0.2);
box-shadow: 0 1px 4px -2px rgba(0,0,0,0.2);
backdrop-filter: blur(2px);
position: sticky;
z-index: 100;
padding: 0;
top: 0;
background-color: hsla(0, 0%, 100%, 0.8);
text-align: left;
}
.taskmgr-taskarea table th > span {
display: inline-block;
width: 100%;
/* we set borders on this span because */
/* borders fly away from sticky headers */
border-bottom: 1px solid #e0e0e0;
/* padding order: top right bottom left */
padding:
calc(10 * var(--scale))
calc(2.5 * var(--scale))
calc(5 * var(--scale))
calc(2.5 * var(--scale));
}
.taskmgr-taskarea table th:not(:last-of-type) > span {
/* we set borders on this span because */
/* borders fly away from sticky headers */
border-right: 1px solid #e0e0e0;
}
.taskmgr-taskarea table td {
border-bottom: 1px solid #e0e0e0;
}
.taskmgr-taskarea table td > span {
padding: 0 calc(2.5 * var(--scale));
}
.taskmgr-indentcell {
position: relative;
align-items: right;
width: calc(10 * var(--scale));
height: calc(10 * var(--scale));
}
.taskmgr-indentcell-trunk {
position: absolute;
top: 0;
left: calc(5 * var(--scale));
width: calc(5 * var(--scale));
height: calc(10 * var(--scale));
border-left: 2px solid var(--line-color);
}
.taskmgr-indentcell-branch {
position: absolute;
top: 0;
left: calc(5 * var(--scale));
width: calc(5 * var(--scale));
height: calc(5 * var(--scale));
border-left: 2px solid var(--line-color);
border-bottom: 2px solid var(--line-color);
border-radius: 0 0 0 calc(2.5 * var(--scale));
}
.taskmgr-task {
display: flex;
height: calc(10 * var(--scale));
line-height: calc(10 * var(--scale));
}
.taskmgr-task-title {
flex-grow: 1;
padding-left: calc(2.5 * var(--scale));
}