mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-27 03:42:34 +00:00
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:
+292
-214
@@ -2,11 +2,291 @@ import { END_HARD, END_SOFT } from "../definitions.js";
|
||||
import UIAlert from "./UIAlert.js";
|
||||
import UIContextMenu from "./UIContextMenu.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 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'),
|
||||
icon: globalThis.icons['cog.svg'],
|
||||
uid: null,
|
||||
@@ -40,220 +320,18 @@ const UIWindowTaskManager = async function UIWindowTaskManager () {
|
||||
var(--primary-lightness),
|
||||
var(--primary-alpha))`,
|
||||
'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;
|
||||
|
||||
Reference in New Issue
Block a user