chore: frontend updates

This commit is contained in:
jamesread 2025-09-07 01:33:46 +01:00
parent 570c0ba087
commit 7345744e41
14 changed files with 524 additions and 1258 deletions

View File

@ -8,7 +8,6 @@
<title>OliveTin</title> <title>OliveTin</title>
<link rel = "stylesheet" type = "text/css" href = "/style.css" />
<link rel = "stylesheet" type = "text/css" href = "/theme.css" /> <link rel = "stylesheet" type = "text/css" href = "/theme.css" />
<link rel = "stylesheet" href = "node_modules/@xterm/xterm/css/xterm.css" /> <link rel = "stylesheet" href = "node_modules/@xterm/xterm/css/xterm.css" />

View File

@ -1,6 +1,7 @@
'use strict' 'use strict'
import 'femtocrank/style.css' import 'femtocrank/style.css'
import '/style.css'
import { createClient } from '@connectrpc/connect' import { createClient } from '@connectrpc/connect'
import { createConnectTransport } from '@connectrpc/connect-web' import { createConnectTransport } from '@connectrpc/connect-web'

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,8 @@
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1", "eslint-plugin-promise": "^4.3.1",
"process": "^0.11.10", "process": "^0.11.10",
"stylelint": "^15.6.0", "stylelint": "^16.23.1",
"stylelint-config-standard": "^33.0.0" "stylelint-config-standard": "^39.0.0"
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
@ -27,16 +27,16 @@
], ],
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@connectrpc/connect": "^2.0.3", "@connectrpc/connect": "^2.1.0",
"@connectrpc/connect-web": "^2.0.3", "@connectrpc/connect-web": "^2.1.0",
"@hugeicons/core-free-icons": "^1.0.16", "@hugeicons/core-free-icons": "^1.0.16",
"@hugeicons/vue": "^1.0.3", "@hugeicons/vue": "^1.0.3",
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"femtocrank": "^1.2.4", "picocrank": "^1.3.1",
"unplugin-vue-components": "^28.8.0", "unplugin-vue-components": "^29.0.0",
"vite": "^7.0.6", "vite": "^7.1.4",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
} }
} }

View File

@ -11,10 +11,6 @@
</button> </button>
</div> </div>
<div class="fg1">
<Breadcrumbs />
</div>
<div id="banner" v-if="bannerMessage" :style="bannerCss"> <div id="banner" v-if="bannerMessage" :style="bannerCss">
<p>{{ bannerMessage }}</p> <p>{{ bannerMessage }}</p>
</div> </div>
@ -65,10 +61,11 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import Sidebar from './components/Sidebar.vue'; import Sidebar from 'picocrank/vue/components/Sidebar.vue';
import { HugeiconsIcon } from '@hugeicons/vue' import { HugeiconsIcon } from '@hugeicons/vue'
import { Menu01Icon } from '@hugeicons/core-free-icons' import { Menu01Icon } from '@hugeicons/core-free-icons'
import { UserCircle02Icon } from '@hugeicons/core-free-icons' import { UserCircle02Icon } from '@hugeicons/core-free-icons'
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
const sidebar = ref(null); const sidebar = ref(null);
const username = ref('guest'); const username = ref('guest');
@ -88,22 +85,29 @@ async function requestInit() {
try { try {
const initResponse = await window.client.init({}) const initResponse = await window.client.init({})
console.log("init response", initResponse)
username.value = initResponse.authenticatedUser username.value = initResponse.authenticatedUser
currentVersion.value = initResponse.currentVersion currentVersion.value = initResponse.currentVersion
bannerMessage.value = initResponse.bannerMessage || ''; bannerMessage.value = initResponse.bannerMessage || '';
bannerCss.value = initResponse.bannerCss || ''; bannerCss.value = initResponse.bannerCss || '';
sidebar.value.addRouterLink('Actions')
for (const rootDashboard of initResponse.rootDashboards) { for (const rootDashboard of initResponse.rootDashboards) {
sidebar.value.addNavigationLink({ sidebar.value.addNavigationLink({
id: rootDashboard, id: rootDashboard,
name: rootDashboard,
title: rootDashboard, title: rootDashboard,
path: `/dashboards/${rootDashboard}`, path: `/dashboards/${rootDashboard}`,
icon: '📊' icon: DashboardSquare01Icon,
}) })
} }
sidebar.value.addSeparator()
sidebar.value.addRouterLink('Entities')
sidebar.value.addRouterLink('Logs')
sidebar.value.addRouterLink('Diagnostics')
hasLoaded.value = true; hasLoaded.value = true;
} catch (error) { } catch (error) {
console.error("Error initializing client", error) console.error("Error initializing client", error)

View File

@ -59,7 +59,6 @@ const props = defineProps({
const dashboard = ref(null) const dashboard = ref(null)
async function getDashboard() { async function getDashboard() {
console.log("getting dashboard", props.title)
const ret = await window.client.getDashboard({ const ret = await window.client.getDashboard({
title: props.title, title: props.title,
}) })

View File

@ -1,246 +0,0 @@
<template>
<aside :class="{ 'shown': isOpen, 'stuck': isStuck }" class="sidebar">
<div class = "flex-row">
<h2>Navigation</h2>
<div class = "fg1" />
<button
class="stick-toggle"
:aria-pressed="isStuck"
:title="isStuck ? 'Unstick sidebar' : 'Stick sidebar'"
@click="toggleStick"
>
<span v-if="isStuck">
<HugeiconsIcon :icon="Pin02Icon" width = "1em" height = "1em" />
</span>
<span v-else>
<HugeiconsIcon :icon="PinIcon" width = "1em" height = "1em" />
</span>
</button>
</div>
<nav class="mainnav">
<ul class="navigation-links">
<li v-for="link in navigationLinks" :key="link.id" :title="link.title">
<router-link :to="link.path" :class="{ active: isActive(link.path) }">
<HugeiconsIcon :icon="link.icon" />
<span>{{ link.title }}</span>
</router-link>
</li>
</ul>
<ul class="supplemental-links">
<li v-for="link in supplementalLinks" :key="link.id" :title="link.title">
<router-link :to="link.path" :class="{ active: isActive(link.path) }">
<HugeiconsIcon :icon="link.icon" />
<span>{{ link.title }}</span>
</router-link>
</li>
</ul>
</nav>
</aside>
</template>
<script setup>
import { ref, onMounted, getCurrentInstance } from 'vue'
import { useRoute } from 'vue-router'
import { HugeiconsIcon } from '@hugeicons/vue'
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
import { LeftToRightListDashIcon } from '@hugeicons/core-free-icons'
import { Wrench01Icon } from '@hugeicons/core-free-icons'
import { Pin02Icon } from '@hugeicons/core-free-icons'
import { PinIcon } from '@hugeicons/core-free-icons'
import { CellsIcon } from '@hugeicons/core-free-icons'
const isOpen = ref(false)
const isStuck = ref(false)
const navigationLinks = ref([
{
id: 'actions',
title: 'Actions',
path: '/',
icon: DashboardSquare01Icon,
}
])
const supplementalLinks = ref([
{
id: 'entities',
title: 'Entities',
path: '/entities',
icon: CellsIcon,
},
{
id: 'logs',
title: 'Logs',
path: '/logs',
icon: LeftToRightListDashIcon,
},
{
id: 'diagnostics',
title: 'Diagnostics',
path: '/diagnostics',
icon: Wrench01Icon,
}
])
const route = useRoute()
function toggleStick() {
isStuck.value = !isStuck.value
}
function toggle() {
isOpen.value = !isOpen.value
isStuck.value = false
}
function open() {
isOpen.value = true
}
function close() {
isOpen.value = false
isStuck.value = false
}
function isActive(path) {
return route.path === path
}
// Method to add navigation links from other components
function addNavigationLink(link) {
link.icon = DashboardSquare01Icon
const existingIndex = navigationLinks.value.findIndex(l => l.id === link.id)
if (existingIndex >= 0) {
navigationLinks.value[existingIndex] = { ...link }
} else {
navigationLinks.value.push({ ...link })
}
}
// Method to add supplemental links from other components
function addSupplementalLink(link) {
const existingIndex = supplementalLinks.value.findIndex(l => l.id === link.id)
if (existingIndex >= 0) {
supplementalLinks.value[existingIndex] = { ...link }
} else {
supplementalLinks.value.push({ ...link })
}
}
// Method to remove links
function removeNavigationLink(linkId) {
navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
}
function removeSupplementalLink(linkId) {
supplementalLinks.value = supplementalLinks.value.filter(link => link.id !== linkId)
}
// Method to clear all links
function clearNavigationLinks() {
navigationLinks.value = []
}
function clearSupplementalLinks() {
supplementalLinks.value = []
}
// Method to get all links
function getNavigationLinks() {
return [...navigationLinks.value]
}
function getSupplementalLinks() {
return [...supplementalLinks.value]
}
onMounted(() => {
// Make the sidebar globally accessible
window.sidebar = {
get isOpen() { return isOpen.value },
set isOpen(val) { isOpen.value = val },
toggle,
open,
close,
addNavigationLink,
addSupplementalLink,
removeNavigationLink,
removeSupplementalLink,
clearNavigationLinks,
clearSupplementalLinks,
getNavigationLinks,
getSupplementalLinks
}
})
defineExpose({
isOpen,
navigationLinks,
supplementalLinks,
toggle,
open,
close,
isActive,
addNavigationLink,
addSupplementalLink,
removeNavigationLink,
removeSupplementalLink,
clearNavigationLinks,
clearSupplementalLinks,
getNavigationLinks,
getSupplementalLinks
})
</script>
<style scoped>
.active {
text-decoration: underline;
}
li {
margin: 0;
padding: 0;
}
.navigation-links a,
.supplemental-links a {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: #333;
transition: background-color 0.2s ease;
border-left: 3px solid transparent;
}
.navigation-links a:hover,
.supplemental-links a:hover {
background: #f8f9fa;
color: #007bff;
}
.icon {
font-size: 1.2em;
width: 1.5rem;
text-align: center;
}
.supplemental-links {
border-top: 1px solid #eee;
margin-top: 1rem;
}
/* Responsive design */
@media (max-width: 768px) {
.sidebar {
width: 100%;
left: -100%;
}
.sidebar.shown {
left: 0;
}
}
</style>

View File

@ -1,12 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { Wrench01Icon } from '@hugeicons/core-free-icons'
import { LeftToRightListDashIcon } from '@hugeicons/core-free-icons'
import { CellsIcon } from '@hugeicons/core-free-icons'
import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
const routes = [ const routes = [
{ {
path: '/', path: '/',
name: 'Home', name: 'Actions',
component: () => import('./Dashboard.vue'), component: () => import('./Dashboard.vue'),
props: { title: 'default' }, props: { title: 'default' },
meta: { title: 'OliveTin - Dashboard' } meta: { title: 'Actions', icon: DashboardSquare01Icon }
}, },
{ {
path: '/dashboards/:title', path: '/dashboards/:title',
@ -26,13 +31,19 @@ const routes = [
path: '/logs', path: '/logs',
name: 'Logs', name: 'Logs',
component: () => import('./views/LogsListView.vue'), component: () => import('./views/LogsListView.vue'),
meta: { title: 'OliveTin - Logs' } meta: {
title: 'OliveTin - Logs',
icon: LeftToRightListDashIcon
}
}, },
{ {
path: '/entities', path: '/entities',
name: 'Entities', name: 'Entities',
component: () => import('./views/EntitiesView.vue'), component: () => import('./views/EntitiesView.vue'),
meta: { title: 'OliveTin - Entities' } meta: {
title: 'OliveTin - Entities',
icon: CellsIcon
}
}, },
{ {
path: '/entity-details/:entityType/:entityKey', path: '/entity-details/:entityType/:entityKey',
@ -64,7 +75,10 @@ const routes = [
path: '/diagnostics', path: '/diagnostics',
name: 'Diagnostics', name: 'Diagnostics',
component: () => import('./views/DiagnosticsView.vue'), component: () => import('./views/DiagnosticsView.vue'),
meta: { title: 'OliveTin - Diagnostics' } meta: {
title: 'OliveTin - Diagnostics',
icon: Wrench01Icon
}
}, },
{ {
path: '/login', path: '/login',

View File

@ -1,55 +1,40 @@
<template> <template>
<section> <Section title = "Get support">
<div class="section-header"> <p>If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include a sosreport from this page.
<h2>Get support</h2> </p>
</div> <ul>
<div class="section-content"> <li>
<p>If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include a sosreport from this page. <a href="https://docs.olivetin.app/sosreport.html" target="_blank">sosreport Documentation</a>
</p> </li>
<ul> <li>
<li> <a href = "https://docs.olivetin.app/troubleshooting/wheretofindhelp.html" target="_blank">Where to find help</a>
<a href="https://docs.olivetin.app/sosreport.html" target="_blank">sosreport Documentation</a> </li>
</li> </ul>
<li> </Section>
<a href = "https://docs.olivetin.app/troubleshooting/wheretofindhelp.html" target="_blank">Where to find help</a>
</li>
</ul>
</div>
</section>
<section> <Section title = "SSH">
<div class="section-header"> <dl>
<h2>SSH</h2> <dt>Found Key</dt>
</div> <dd>{{ diagnostics.sshFoundKey || '?' }}</dd>
<div class="section-content"> <dt>Found Config</dt>
<dl> <dd>{{ diagnostics.sshFoundConfig || '?' }}</dd>
<dt>Found Key</dt> </dl>
<dd>{{ diagnostics.sshFoundKey || '?' }}</dd> </Section>
<dt>Found Config</dt>
<dd>{{ diagnostics.sshFoundConfig || '?' }}</dd>
</dl>
</div>
</section>
<section> <Section title = "SOS Report">
<div class="section-header"> <p>This section allows you to generate a detailed report of your configuration and environment. It is a good idea to include this when raising a support request.</p>
<h2>SOS Report</h2>
<div role="toolbar">
<button @click="generateSosReport" :disabled="loading" class = "good">Generate SOS Report</button>
</div> </div>
<div class="section-content">
<p>This section allows you to generate a detailed report of your configuration and environment. It is a good idea to include this when raising a support request.</p> <textarea v-model="sosReport" readonly style="flex: 1; min-height: 200px; resize: vertical;"></textarea>
</Section>
<div role="toolbar">
<button @click="generateSosReport" :disabled="loading" class = "good">Generate SOS Report</button>
</div>
<textarea v-model="sosReport" readonly style="flex: 1; min-height: 200px; resize: vertical;"></textarea>
</div>
</section>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import Section from 'picocrank/vue/components/Section.vue'
const diagnostics = ref({}) const diagnostics = ref({})
const loading = ref(false) const loading = ref(false)

View File

@ -1,17 +1,13 @@
<template> <template>
<section class = "with-header-and-content" v-if="entityDefinitions.length === 0"> <Section class = "with-header-and-content" v-if="entityDefinitions.length === 0" title="Loading entity definitions...">
<div class = "section-header"> <div class = "section-header">
<h2 class="loading-message"> <h2 class="loading-message">
Loading entity definitions... Loading entity definitions...
</h2> </h2>
</div> </div>
</section> </Section>
<template v-else> <template v-else>
<section v-for="def in entityDefinitions" :key="def.name" class="with-header-and-content"> <Section v-for="def in entityDefinitions" :key="def.name" :title="'Entity: ' + def.title ">
<div class = "section-header">
<h2>Entity: {{ def.title }}</h2>
</div>
<div class = "section-content"> <div class = "section-content">
<p>{{ def.instances.length }} instances.</p> <p>{{ def.instances.length }} instances.</p>
@ -32,12 +28,13 @@
</li> </li>
</ul> </ul>
</div> </div>
</section> </Section>
</template> </template>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import Section from 'picocrank/vue/components/Section.vue'
const entityDefinitions = ref([]) const entityDefinitions = ref([])

View File

@ -1,19 +1,16 @@
<template> <template>
<section class="with-header-and-content"> <Section title="Entity Details">
<div class="section-header"> <div>
<h2>Entity Details</h2>
</div>
<div class="section-content">
<p v-if="!entityDetails">Loading entity details...</p> <p v-if="!entityDetails">Loading entity details...</p>
<p v-else-if="!entityDetails.title">No details available for this entity.</p> <p v-else-if="!entityDetails.title">No details available for this entity.</p>
<p v-else>{{ entityDetails.title }}</p> <p v-else>{{ entityDetails.title }}</p>
</div> </div>
</section> </Section>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue' import { ref, onMounted } from 'vue'
import Section from 'picocrank/vue/components/Section.vue'
const entityDetails = ref(null) const entityDetails = ref(null)

View File

@ -1,23 +1,16 @@
<template> <template>
<section class="with-header-and-content"> <Section :title="'Execution Results: ' + title">
<div class="section-header"> <template #toolbar>
<h2>Execution Results: {{ title }}</h2>
<button @click="toggleSize" title="Toggle dialog size"> <button @click="toggleSize" title="Toggle dialog size">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor" <path fill="currentColor"
d="M3 3h6v2H6.462l4.843 4.843l-1.415 1.414L5 6.367V9H3zm0 18h6v-2H6.376l4.929-4.928l-1.415-1.414L5 17.548V15H3zm12 0h6v-6h-2v2.524l-4.867-4.866l-1.414 1.414L17.647 19H15zm6-18h-6v2h2.562l-4.843 4.843l1.414 1.414L19 6.39V9h2z" /> d="M3 3h6v2H6.462l4.843 4.843l-1.415 1.414L5 6.367V9H3zm0 18h6v-2H6.376l4.929-4.928l-1.415-1.414L5 17.548V15H3zm12 0h6v-6h-2v2.524l-4.867-4.866l-1.414 1.414L17.647 19H15zm6-18h-6v2h2.562l-4.843 4.843l1.414 1.414L19 6.39V9h2z" />
</svg> </svg>
</button> </button>
</template>
</div> <div v-if="logEntry" class = "flex-row">
<div class="section-content"> <dl class = "fg1">
<div v-if="logEntry">
<div class="action-header padded-content" style="float: right">
<span class="icon" role="img" v-html="icon"></span>
</div>
<dl>
<dt>Duration</dt> <dt>Duration</dt>
<dd><span v-html="duration"></span></dd> <dd><span v-html="duration"></span></dd>
@ -26,9 +19,10 @@
<ActionStatusDisplay :log-entry="logEntry" /> <ActionStatusDisplay :log-entry="logEntry" />
</dd> </dd>
</dl> </dl>
</div> <span class="icon" role="img" v-html="icon" style = "align-self: start"></span>
</div>
<div ref="xtermOutput"></div> <div ref="xtermOutput"></div>
<br /> <br />
@ -49,13 +43,14 @@
Kill Kill
</button> </button>
</div> </div>
</div>
</section> </Section>
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, onBeforeUnmount, nextTick, watch } from 'vue' import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue' import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
import Section from 'picocrank/vue/components/Section.vue'
import { OutputTerminal } from '../../../js/OutputTerminal.js' import { OutputTerminal } from '../../../js/OutputTerminal.js'
import { HugeiconsIcon } from '@hugeicons/vue' import { HugeiconsIcon } from '@hugeicons/vue'
import { WorkoutRunIcon, Cancel02Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons' import { WorkoutRunIcon, Cancel02Icon, ArrowLeftIcon } from '@hugeicons/core-free-icons'
@ -66,7 +61,6 @@ const router = useRouter()
// Refs for DOM elements // Refs for DOM elements
const xtermOutput = ref(null) const xtermOutput = ref(null)
const dialog = ref(null)
const props = defineProps({ const props = defineProps({
executionTrackingId: { executionTrackingId: {

View File

@ -1,9 +1,6 @@
<template> <template>
<section> <Section title="Logs" :padding="false">
<div class="section-header"> <template #toolbar>
<h2>Logs</h2>
<div>
<label class="input-with-icons"> <label class="input-with-icons">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor" <path fill="currentColor"
@ -17,12 +14,9 @@
</svg> </svg>
</button> </button>
</label> </label>
</div> </template>
</div> <p class = "padding">This is a list of logs from actions that have been executed. You can filter the list by action title.</p>
<div class="section-content">
<p>This is a list of logs from actions that have been executed. You can filter the list by action title.</p>
<div v-show="filteredLogs.length > 0"> <div v-show="filteredLogs.length > 0">
<table class="logs-table"> <table class="logs-table">
<thead> <thead>
@ -60,7 +54,7 @@
</tbody> </tbody>
</table> </table>
<Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" @page-change="handlePageChange" <Pagination :pageSize="pageSize" :total="totalCount" :currentPage="currentPage" @page-change="handlePageChange" class = "padding"
@page-size-change="handlePageSizeChange" itemTitle="execution logs" /> @page-size-change="handlePageSizeChange" itemTitle="execution logs" />
</div> </div>
@ -68,13 +62,13 @@
<p>There are no logs to display.</p> <p>There are no logs to display.</p>
<router-link to="/">Return to index</router-link> <router-link to="/">Return to index</router-link>
</div> </div>
</div> </Section>
</section>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import Pagination from '../components/Pagination.vue' import Pagination from '../components/Pagination.vue'
import Section from 'picocrank/vue/components/Section.vue'
const logs = ref([]) const logs = ref([])
const searchText = ref('') const searchText = ref('')

View File

@ -92,10 +92,6 @@ div.buttons button svg {
vertical-align: middle; vertical-align: middle;
} }
section .section-content {
padding-top: 0;
}
footer { footer {
font-size: small; font-size: small;
} }