Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions demo/editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ This is the **editor demo** for monitorat. Any widget that can be edited or conf

### How to Edit

Click the pencil icon next to or on any wiki or card, or create a new entry with "Add Widget item" button, to open the editor.
Use the widget affordances:

- Wiki: hover the wiki header and click the pencil-paper icon
- Reminders: click the header `...` to add a reminder, or hover a card and
click its corner `...` to edit it
- Services: click the header `...` to add a service, or hover a card and click
its corner `...` to open the info modal, then use the wrench to edit
- Metrics: click the `...` in the snapshot header to configure snapshot tiles

### Features

Expand All @@ -21,15 +28,14 @@ Click the pencil icon next to or on any wiki or card, or create a new entry with

### Card Editors

- **Add**: Click the "Add Widget Item" button to add a new display card for
- **Add**: Click the header `...` action to add a new display card for
- System Metrics
- Services
- Reminders
- **Edit**: Click the pencil icon to edit a display card
- **Edit**: Use the card `...` action or modal wrench for widget items
- **Save**: Click Save to persist changes

### Caveats

- **Histories**: Charts and Tables, in addition to their display quantities and dropdowns, are only editable through YAML
- **Notifications**: Apprise URLs for the notification harness are also YAML-only (used by multiple widgets)

21 changes: 20 additions & 1 deletion monitorat/static/controls/listing.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,33 @@ class ListingControls {
initializeAddButton() {
if (!this.addConfig) return;

const addButton = this.container.querySelector(this.selectors.add);
let addButton = this.container.querySelector(this.selectors.add);
if (!addButton) return;

if (this.addConfig.enabled === false) {
addButton.remove();
return;
}

const affordance = this.addConfig.affordance;
if (affordance) {
const controls = window.monitorShared?.EditorControls;
const createButton =
affordance.type === 'edit'
? controls?.createEditButton
: controls?.createOverflowButton;
const nextButton = createButton?.({
className: affordance.className || '',
title: affordance.title || '',
label: affordance.label || '',
visible: affordance.visible !== false,
});
if (nextButton) {
addButton.replaceWith(nextButton);
addButton = nextButton;
}
}

if (typeof this.addConfig.onClick === 'function') {
addButton.addEventListener('click', () => this.addConfig.onClick());
}
Expand Down
104 changes: 95 additions & 9 deletions monitorat/static/editor/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -159,33 +159,119 @@
font-weight: 500;
}

.editor-edit-btn {
.editor-affordance-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
margin: 0;
border: none;
padding: 4px;
border-radius: 999px;
border-radius: 8px;
background: transparent;
color: var(--text-muted);
cursor: pointer;
opacity: 0.42;
transition:
background 0.15s ease,
background-color 0.15s ease,
opacity 0.15s ease,
color 0.15s ease;
width: 28px;
height: 28px;
margin-left: 8px;
}

.editor-edit-btn svg {
.editor-affordance-btn svg {
width: 16px;
height: 16px;
display: block;
transition: transform 0.15s ease;
}

.editor-affordance-btn:hover,
.editor-affordance-btn:focus-visible {
background: color-mix(in srgb, var(--text-primary) 12%, transparent);
opacity: 1;
color: var(--text-primary);
}

.editor-affordance-btn:hover svg,
.editor-affordance-btn:focus-visible svg {
transform: scale(1.14);
}

.editor-affordance-btn:focus-visible {
outline: 2px solid var(--border-muted);
outline-offset: 2px;
}

.editor-affordance-visible {
opacity: 0.34;
}

.editor-affordance-reveal-parent {
position: relative;
}

.editor-affordance-corner {
position: absolute;
top: 6px;
right: 6px;
}

.editor-edit-btn:hover {
.editor-affordance-card-action {
top: 0;
right: 0;
width: 34px;
height: 34px;
border-radius: 0 8px 0 10px;
color: var(--text-muted);
transition: background-color 0.15s ease;
}

.editor-affordance-card-action .editor-affordance-btn {
width: 100%;
height: 100%;
border-radius: inherit;
opacity: 1;
color: inherit;
}

.editor-affordance-card-action .editor-affordance-btn:hover,
.editor-affordance-card-action .editor-affordance-btn:focus-visible {
background: transparent;
}

.editor-affordance-card-action:hover,
.editor-affordance-card-action:focus-within {
color: var(--text-primary);
background: color-mix(in srgb, var(--text-primary) 12%, transparent);
}

.editor-affordance-reveal {
opacity: 0;
pointer-events: none;
transform: scale(0.94);
}

.editor-affordance-reveal-parent:hover .editor-affordance-reveal,
.editor-affordance-reveal-parent:focus-within .editor-affordance-reveal,
.editor-affordance-reveal:hover,
.editor-affordance-reveal:focus-visible {
opacity: 0.58;
pointer-events: auto;
transform: scale(1);
}

.editor-affordance-reveal:hover,
.editor-affordance-reveal:focus-visible {
opacity: 1;
}

@media (hover: none), (pointer: coarse) {
.editor-affordance-reveal {
opacity: 0.42;
pointer-events: auto;
transform: scale(1);
}
}

@media (max-height: 600px) {
Expand Down
86 changes: 84 additions & 2 deletions monitorat/static/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ window.Editor = (() => {
const CHEVRON_UP = '<polyline points="18 15 12 9 6 15"/>';
const ICON_SAVE =
'<svg aria-hidden="true" viewBox="0 0 448 512" fill="currentColor"><path d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM224 416c-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64s64 28.654 64 64c0 35.346-28.654 64-64 64zm96-304.52V212c0 6.627-5.373 12-12 12H76c-6.627 0-12-5.373-12-12V108c0-6.627 5.373-12 12-12h228.52c3.183 0 6.235 1.264 8.485 3.515l3.48 3.48A11.996 11.996 0 0 1 320 111.48z"></path></svg>';
const ICON_RESTORE =
'<svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor"><path fill="none" d="M0 0h24v24H0V0z"></path><path d="M13 3a9 9 0 0 0-9 9H1l4 3.99L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.954 8.954 0 0 0 13 21a9 9 0 0 0 0-18zm-1 5v5l4.25 2.52.77-1.28-3.52-2.09V8z"></path></svg>';
const ICON_DELETE =
'<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>';
const ICON_CANCEL =
Expand Down Expand Up @@ -348,3 +346,87 @@ window.Editor = (() => {
clearDraft,
};
})();

window.monitorShared = window.monitorShared || {};
window.monitorShared.EditorControls = (() => {
function createActionButton(options = {}) {
const {
className = '',
icon = '',
iconName = '',
title = '',
label = '',
visible = false,
state = visible ? 'visible' : 'reveal',
} = options;

const button = document.createElement('button');
button.type = 'button';
button.className = [
'editor-affordance-btn',
state === 'visible'
? 'editor-affordance-visible'
: state === 'reveal'
? 'editor-affordance-reveal'
: '',
className,
]
.filter(Boolean)
.join(' ');
if (title) {
button.title = title;
}
if (label) {
button.setAttribute('aria-label', label);
}
button.innerHTML =
icon || window.monitorShared.IconHandler.getActionIcon(iconName);
return button;
}

function createOverflowButton(options = {}) {
return createActionButton({
...options,
icon:
options.icon ||
window.monitorShared.IconHandler.getActionIcon('overflow'),
});
}

function createEditButton(options = {}) {
return createActionButton({
...options,
iconName: options.iconName || 'edit',
});
}

function wrapCardAction(button, className = '') {
const container = document.createElement('div');
container.className = [
'editor-affordance-card-action',
'editor-affordance-corner',
'editor-affordance-reveal',
className,
]
.filter(Boolean)
.join(' ');
container.appendChild(button);
return { button, container };
}

function createCardOverflowButton(options = {}) {
const { className = '', ...buttonOptions } = options;
const button = createOverflowButton({
...buttonOptions,
state: 'none',
});
return wrapCardAction(button, className);
}

return {
createActionButton,
createCardOverflowButton,
createEditButton,
createOverflowButton,
};
})();
10 changes: 8 additions & 2 deletions monitorat/static/themes/default.css
Original file line number Diff line number Diff line change
Expand Up @@ -945,12 +945,18 @@ h6:hover .header-anchor {

/* Utilities */
.hover-expand {
color: var(--text-muted);
transform: scale(1);
transform-origin: center;
transition: transform 0.15s ease;
transition:
color 0.15s ease,
transform 0.15s ease;
}

.hover-expand:hover,
.hover-expand-parent:hover .hover-expand {
.hover-expand:focus-visible,
.hover-expand-parent:hover .hover-expand,
.hover-expand-parent:focus-within .hover-expand {
color: var(--text-primary);
transform: scale(1.12);
}
17 changes: 16 additions & 1 deletion monitorat/static/ui/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
*/

const IconHandler = (() => {
const ACTION_ICONS = {
edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>',
'edit-doc':
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
overflow:
'<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="6.5" cy="12" r="1.75"/><circle cx="12" cy="12" r="1.75"/><circle cx="17.5" cy="12" r="1.75"/></svg>',
wrench:
'<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke-width="2" d="M16,15 C20.0089021,14.9354541 23,11.9673591 23,8 C23,4.98813056 22.0029673,5.9851632 21,7 C20.0089021,7.97922849 18,10 18,10 L14,6 C14,6 16.0207715,3.99109792 17,3 C18.0148368,1.99703264 18.0148368,1 16,1 C12.0326409,0.999999999 9.05307486,3.99109792 9,8 C9.04154304,8.97626113 9,11 9,11 C7.11486635,12.8970031 4.65923194,15.3526375 3,17 C0.0682492584,19.9436202 4.05637975,23.9317507 7,21 C8.65052042,19.3376102 11.1126942,16.8754364 13,15 C13,15 15.0237389,14.958457 16,15 Z"></path></svg>',
};

function getFileExtension(path) {
return path?.split('.')?.pop()?.toLowerCase() || '';
}
Expand Down Expand Up @@ -93,13 +103,18 @@ const IconHandler = (() => {
const parser = new DOMParser();
const parsed = parser.parseFromString(svgText, 'image/svg+xml');
const root = parsed.documentElement;
if (root && root.tagName.toLowerCase().endsWith('svg')) {
if (root?.tagName.toLowerCase().endsWith('svg')) {
return root;
}
return null;
}

function getActionIcon(name) {
return ACTION_ICONS[name] || '';
}

return {
getActionIcon,
renderIcon,
};
})();
Expand Down
21 changes: 12 additions & 9 deletions monitorat/widgets/metrics/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,19 +294,22 @@ class MetricsWidget {
}

addEditButton() {
const controlsRow = this.container.querySelector(
'[data-metrics="widget-controls"]',
const snapshotHeader = this.container.querySelector(
'[data-metrics-section-header="snapshot"]',
);
if (!controlsRow) return;

controlsRow.style.display = '';
const configureBtn = controlsRow.querySelector('.metrics-configure');
if (!configureBtn) return;

configureBtn.style.display = '';
if (!snapshotHeader) return;

const controls = window.monitorShared?.EditorControls;
const configureBtn =
controls?.createOverflowButton({
visible: true,
title: 'Configure metrics',
label: 'Configure metrics',
}) || document.createElement('button');
configureBtn.addEventListener('click', () => {
this.openMetricsEditor();
});
snapshotHeader.appendChild(configureBtn);
}

setupEventListeners() {
Expand Down
2 changes: 1 addition & 1 deletion monitorat/widgets/metrics/features/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class MetricsEditor {
file: sourcePath,
content: '',
useForm: true,
title: 'Snapshot Tiles',
title: 'Metrics Snapshot Tiles',
labels: { edit: 'Configure', preview: 'Preview' },
onSave: async () => {
const payload = this.collectQuantities(formContainer);
Expand Down
Loading