This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit 760a1006e7012676320477c12662f898ffcb7131 Author: Wu Sheng <[email protected]> AuthorDate: Tue May 12 10:16:49 2026 +0800 ui: appshell with sidebar topbar and design tokens --- apps/ui/src/App.vue | 20 +++- apps/ui/src/components/icons/Icon.vue | 177 ++++++++++++++++++++++++++++ apps/ui/src/components/shell/AppShell.vue | 31 +++++ apps/ui/src/components/shell/AppSidebar.vue | 171 +++++++++++++++++++++++++++ apps/ui/src/components/shell/AppTopbar.vue | 62 ++++++++++ apps/ui/src/views/landing/LandingView.vue | 47 +++++--- 6 files changed, 489 insertions(+), 19 deletions(-) diff --git a/apps/ui/src/App.vue b/apps/ui/src/App.vue index 69dd87a..944e2c4 100644 --- a/apps/ui/src/App.vue +++ b/apps/ui/src/App.vue @@ -1,7 +1,23 @@ +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> <script setup lang="ts"> -import { RouterView } from 'vue-router'; +import AppShell from '@/components/shell/AppShell.vue'; </script> <template> - <RouterView /> + <AppShell /> </template> diff --git a/apps/ui/src/components/icons/Icon.vue b/apps/ui/src/components/icons/Icon.vue new file mode 100644 index 0000000..4c905e0 --- /dev/null +++ b/apps/ui/src/components/icons/Icon.vue @@ -0,0 +1,177 @@ +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<script setup lang="ts"> +defineProps<{ name: IconName; size?: number }>(); + +export type IconName = + | 'dash' + | 'topo' + | 'metric' + | 'trace' + | 'log' + | 'alert' + | 'prof' + | 'svc' + | 'ep' + | 'event' + | 'set' + | 'search' + | 'chev' + | 'caret' + | 'bell' + | 'plus' + | 'share' + | 'refresh' + | 'more' + | 'clock' + | 'filter' + | 'expand' + | 'download' + | 'star' + | 'flame' + | 'sky' + | 'user'; +</script> + +<template> + <svg + viewBox="0 0 24 24" + :width="size ?? 14" + :height="size ?? 14" + fill="none" + stroke="currentColor" + stroke-width="1.6" + > + <template v-if="name === 'dash'"> + <rect x="3" y="3" width="7" height="9" rx="1.5" /> + <rect x="14" y="3" width="7" height="5" rx="1.5" /> + <rect x="14" y="12" width="7" height="9" rx="1.5" /> + <rect x="3" y="16" width="7" height="5" rx="1.5" /> + </template> + <template v-else-if="name === 'topo'"> + <circle cx="5" cy="6" r="2" /> + <circle cx="19" cy="6" r="2" /> + <circle cx="12" cy="14" r="2" /> + <circle cx="5" cy="20" r="2" /> + <circle cx="19" cy="20" r="2" /> + <path d="M6.5 7.5L10.5 13M17.5 7.5L13.5 13M10.5 15L6.5 19M13.5 15L17.5 19" /> + </template> + <template v-else-if="name === 'metric'"> + <path d="M3 17l5-7 4 4 4-8 5 11" /> + </template> + <template v-else-if="name === 'trace'"> + <path d="M3 6h10M3 10h14M3 14h7M3 18h12" /> + </template> + <template v-else-if="name === 'log'"> + <rect x="4" y="3" width="16" height="18" rx="2" /> + <path d="M8 8h8M8 12h8M8 16h5" /> + </template> + <template v-else-if="name === 'alert'"> + <path d="M12 3l9 16H3z" /> + <path d="M12 10v4M12 17v.5" /> + </template> + <template v-else-if="name === 'prof'"> + <path d="M3 20h18M5 20v-6M9 20v-9M13 20v-4M17 20v-12M21 20v-7" /> + </template> + <template v-else-if="name === 'svc'"> + <rect x="3" y="4" width="18" height="6" rx="1.5" /> + <rect x="3" y="14" width="18" height="6" rx="1.5" /> + <circle cx="7" cy="7" r="0.8" fill="currentColor" /> + <circle cx="7" cy="17" r="0.8" fill="currentColor" /> + </template> + <template v-else-if="name === 'ep'"> + <path d="M4 12h6M14 12h6" /> + <circle cx="12" cy="12" r="2" /> + <path d="M4 12a8 8 0 0116 0M4 12a8 8 0 0016 0" opacity=".4" /> + </template> + <template v-else-if="name === 'event'"> + <circle cx="12" cy="12" r="9" /> + <path d="M12 7v5l3 2" /> + </template> + <template v-else-if="name === 'set'"> + <circle cx="12" cy="12" r="3" /> + <path + d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" + /> + </template> + <template v-else-if="name === 'search'"> + <circle cx="11" cy="11" r="6" /> + <path d="M20 20l-3.5-3.5" /> + </template> + <template v-else-if="name === 'chev'"> + <path stroke-width="2" d="M9 6l6 6-6 6" /> + </template> + <template v-else-if="name === 'caret'"> + <path stroke-width="2" d="M6 9l6 6 6-6" /> + </template> + <template v-else-if="name === 'bell'"> + <path d="M6 8a6 6 0 0112 0v5l1.5 3H4.5L6 13z" /> + <path d="M10 19a2 2 0 004 0" /> + </template> + <template v-else-if="name === 'plus'"> + <path stroke-width="2" d="M12 5v14M5 12h14" /> + </template> + <template v-else-if="name === 'share'"> + <path d="M4 12v6a2 2 0 002 2h12a2 2 0 002-2v-6M16 6l-4-4-4 4M12 2v13" /> + </template> + <template v-else-if="name === 'refresh'"> + <path d="M20 11A8 8 0 006 5l-2 2M4 13a8 8 0 0014 6l2-2M4 4v5h5M20 20v-5h-5" /> + </template> + <template v-else-if="name === 'more'"> + <circle cx="6" cy="12" r="1.4" fill="currentColor" stroke="none" /> + <circle cx="12" cy="12" r="1.4" fill="currentColor" stroke="none" /> + <circle cx="18" cy="12" r="1.4" fill="currentColor" stroke="none" /> + </template> + <template v-else-if="name === 'clock'"> + <circle cx="12" cy="12" r="9" /> + <path d="M12 7v5l3 2" /> + </template> + <template v-else-if="name === 'filter'"> + <path d="M3 5h18l-7 9v6l-4-2v-4z" /> + </template> + <template v-else-if="name === 'expand'"> + <path d="M9 3H3v6M21 9V3h-6M15 21h6v-6M3 15v6h6" /> + </template> + <template v-else-if="name === 'download'"> + <path d="M12 3v12M7 10l5 5 5-5M4 21h16" /> + </template> + <template v-else-if="name === 'star'"> + <path d="M12 3l2.8 6 6.5.9-4.7 4.6 1.1 6.5L12 18l-5.7 3 1.1-6.5L2.7 9.9 9.2 9z" /> + </template> + <template v-else-if="name === 'flame'"> + <path d="M12 3c1 4 6 5 6 11a6 6 0 11-12 0c0-3 2-4 2-7 2 2 3 3 4 0z" /> + </template> + <template v-else-if="name === 'sky'"> + <path + fill="currentColor" + stroke="none" + d="M3 14c4-3 8-3 12-1 3 1.4 5 .5 6-1-1 5-4 8-9 8-4 0-7-2-9-6z" + opacity=".95" + /> + <path + fill="#fff" + stroke="none" + d="M5 10c3-2 7-2 11 0 3 1.3 5 .6 6-1-1 3.6-4 6-8 6-4 0-7-1.6-9-5z" + opacity=".22" + /> + </template> + <template v-else-if="name === 'user'"> + <circle cx="12" cy="8" r="4" /> + <path d="M4 21c0-4 4-6 8-6s8 2 8 6" /> + </template> + </svg> +</template> diff --git a/apps/ui/src/components/shell/AppShell.vue b/apps/ui/src/components/shell/AppShell.vue new file mode 100644 index 0000000..6482234 --- /dev/null +++ b/apps/ui/src/components/shell/AppShell.vue @@ -0,0 +1,31 @@ +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<script setup lang="ts"> +import { RouterView } from 'vue-router'; +import AppSidebar from './AppSidebar.vue'; +import AppTopbar from './AppTopbar.vue'; +</script> + +<template> + <div class="sw"> + <AppSidebar /> + <AppTopbar /> + <main class="sw-main"> + <RouterView /> + </main> + </div> +</template> diff --git a/apps/ui/src/components/shell/AppSidebar.vue b/apps/ui/src/components/shell/AppSidebar.vue new file mode 100644 index 0000000..0d63822 --- /dev/null +++ b/apps/ui/src/components/shell/AppSidebar.vue @@ -0,0 +1,171 @@ +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<script setup lang="ts"> +import { ref } from 'vue'; +import { RouterLink, useRoute } from 'vue-router'; +import Icon, { type IconName } from '@/components/icons/Icon.vue'; + +// Phase 2 will replace this stub with real getMenuItems / listLayers data. +const layers = ref([ + { key: 'general', name: 'General Service', svc: 84, color: 'var(--sw-accent)' }, + { key: 'mesh', name: 'Service Mesh', svc: 22, color: 'var(--sw-info)' }, + { key: 'k8s', name: 'Kubernetes', svc: 62, color: 'var(--sw-purple)' }, + { key: 'rum', name: 'Browser (RUM)', svc: 8, color: 'var(--sw-cyan)' }, + { key: 'mq', name: 'Virtual MQ', svc: 6, color: 'var(--sw-ok)' }, + { key: 'db', name: 'Virtual Database', svc: 6, color: 'var(--sw-warn)' }, + { key: 'otel', name: 'OpenTelemetry', svc: 18, color: 'var(--sw-purple)' }, + { key: 'faas', name: 'FaaS', svc: 3, color: 'var(--sw-err)' }, +]); +const expandedLayer = ref<string | null>('general'); + +const route = useRoute(); +function isActive(path: string): boolean { + return route.path === path || route.path.startsWith(path + '/'); +} + +interface NavRow { + icon: IconName; + label: string; + to: string; + badge?: { text: string; kind?: 'ok' | 'warn' | 'err' | 'info' }; +} + +const telemetry: NavRow[] = [ + { icon: 'metric', label: 'Dashboards', to: '/dashboards' }, + { icon: 'trace', label: 'Traces', to: '/operate/traces' }, + { icon: 'log', label: 'Logs', to: '/operate/logs' }, + { icon: 'prof', label: 'Profiling', to: '/profiling' }, + { icon: 'event', label: 'Events', to: '/operate/events' }, +]; +const operate: NavRow[] = [ + { icon: 'alert', label: 'Alarms', to: '/operate/alarms', badge: { text: '7', kind: 'err' } }, +]; +const admin: NavRow[] = [ + { icon: 'svc', label: 'Cluster status', to: '/cluster' }, + { icon: 'user', label: 'Users', to: '/admin/users' }, + { icon: 'set', label: 'Roles', to: '/admin/roles' }, + { icon: 'log', label: 'Audit log', to: '/admin/audit' }, +]; +</script> + +<template> + <aside class="sw-side"> + <RouterLink to="/" class="sw-brand"> + <div class="sw-brand-mark"><Icon name="sky" :size="13" /></div> + <span>SkyWalking</span> + <small>Horizon</small> + </RouterLink> + + <nav class="sw-nav"> + <div class="sw-nav-section sw-row" style="justify-content: space-between"> + <span>Layers</span> + <span style="color: var(--sw-fg-3); font-weight: 400">{{ layers.length }} layers</span> + </div> + <template v-for="L in layers" :key="L.key"> + <div + class="sw-nav-item" + :class="{ 'is-active': expandedLayer === L.key }" + @click="expandedLayer = expandedLayer === L.key ? null : L.key" + > + <span class="layer-dot" :style="{ background: L.color }" /> + <span :style="{ fontWeight: expandedLayer === L.key ? 600 : 500 }">{{ L.name }}</span> + <span class="sw-badge" style="margin-left: auto">{{ L.svc }}</span> + <span class="caret" :class="{ open: expandedLayer === L.key }"><Icon name="caret" :size="10" /></span> + </div> + <div v-if="expandedLayer === L.key" class="layer-children"> + <RouterLink :to="`/layer/${L.key}`" class="sw-nav-item" :class="{ 'is-active': isActive(`/layer/${L.key}`) }"> + <Icon name="dash" /><span>Layer overview</span> + </RouterLink> + <RouterLink :to="`/layer/${L.key}/services`" class="sw-nav-item" :class="{ 'is-active': isActive(`/layer/${L.key}/services`) }"> + <Icon name="svc" /><span>Services</span><span class="sw-badge" style="margin-left: auto">{{ L.svc }}</span> + </RouterLink> + <RouterLink :to="`/layer/${L.key}/instances`" class="sw-nav-item" :class="{ 'is-active': isActive(`/layer/${L.key}/instances`) }"> + <Icon name="prof" /><span>Instances</span> + </RouterLink> + <RouterLink :to="`/layer/${L.key}/endpoints`" class="sw-nav-item" :class="{ 'is-active': isActive(`/layer/${L.key}/endpoints`) }"> + <Icon name="ep" /><span>Endpoints</span> + </RouterLink> + <RouterLink :to="`/layer/${L.key}/topology`" class="sw-nav-item" :class="{ 'is-active': isActive(`/layer/${L.key}/topology`) }"> + <Icon name="topo" /><span>Topology</span> + </RouterLink> + </div> + </template> + + <div class="sw-nav-section">Telemetry</div> + <RouterLink v-for="row in telemetry" :key="row.to" :to="row.to" class="sw-nav-item" :class="{ 'is-active': isActive(row.to) }"> + <Icon :name="row.icon" /><span>{{ row.label }}</span> + <span v-if="row.badge" class="sw-badge" :class="row.badge.kind" style="margin-left: auto">{{ row.badge.text }}</span> + </RouterLink> + + <div class="sw-nav-section">Operate</div> + <RouterLink v-for="row in operate" :key="row.to" :to="row.to" class="sw-nav-item" :class="{ 'is-active': isActive(row.to) }"> + <Icon :name="row.icon" /><span>{{ row.label }}</span> + <span v-if="row.badge" class="sw-badge" :class="row.badge.kind" style="margin-left: auto">{{ row.badge.text }}</span> + </RouterLink> + + <div class="sw-nav-section">Admin</div> + <RouterLink v-for="row in admin" :key="row.to" :to="row.to" class="sw-nav-item" :class="{ 'is-active': isActive(row.to) }"> + <Icon :name="row.icon" /><span>{{ row.label }}</span> + </RouterLink> + </nav> + + <div class="sw-side-foot"> + <div class="sw-avatar">SW</div> + <div style="line-height: 1.2"> + <div style="color: var(--sw-fg-0); font-weight: 600">guest</div> + <div>not signed in</div> + </div> + </div> + </aside> +</template> + +<style scoped> +.sw-brand, +.sw-brand:hover { + text-decoration: none; + color: inherit; +} +.layer-dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex: 0 0 7px; +} +.caret { + color: var(--sw-fg-3); + margin-left: 4px; + transition: transform 0.15s; + display: inline-flex; + width: 10px; + transform: rotate(-90deg); +} +.caret.open { + transform: rotate(0); +} +.layer-children { + padding-left: 12px; + margin-left: 18px; + margin-bottom: 4px; + border-left: 1px dashed var(--sw-line-2); +} +.layer-children .sw-nav-item { + text-decoration: none; +} +.sw-nav-item { + text-decoration: none; +} +</style> diff --git a/apps/ui/src/components/shell/AppTopbar.vue b/apps/ui/src/components/shell/AppTopbar.vue new file mode 100644 index 0000000..d4f456a --- /dev/null +++ b/apps/ui/src/components/shell/AppTopbar.vue @@ -0,0 +1,62 @@ +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<script setup lang="ts"> +import { computed } from 'vue'; +import { useRoute } from 'vue-router'; +import Icon from '@/components/icons/Icon.vue'; + +const route = useRoute(); + +// Trivial breadcrumb derivation from the path. Real breadcrumb metadata +// lands when individual views start setting `route.meta.breadcrumbs`. +const crumbs = computed<string[]>(() => { + const segs = route.path.split('/').filter(Boolean); + if (segs.length === 0) return ['Home']; + return segs.map((s) => s.replace(/-/g, ' ').replace(/^./, (c) => c.toUpperCase())); +}); +</script> + +<template> + <header class="sw-top"> + <div class="sw-crumbs"> + <template v-for="(c, i) in crumbs" :key="i"> + <Icon v-if="i > 0" name="chev" :size="10" /> + <b v-if="i === crumbs.length - 1">{{ c }}</b> + <span v-else>{{ c }}</span> + </template> + </div> + <div class="sw-top-search"> + <Icon name="search" :size="12" /> + <span>Search services, endpoints, traceId…</span> + <kbd>⌘K</kbd> + </div> + <div class="sw-top-actions"> + <div class="sw-btn"> + <span style="color: var(--sw-fg-2)">env</span> + <b style="color: var(--sw-fg-0)">production</b> + <Icon name="caret" :size="10" /> + </div> + <div class="sw-btn"> + <Icon name="clock" :size="12" /> + <span>Last 30 minutes</span> + <Icon name="caret" :size="10" /> + </div> + <div class="sw-btn is-icon"><Icon name="refresh" :size="12" /></div> + <div class="sw-btn is-icon"><Icon name="bell" :size="12" /></div> + </div> + </header> +</template> diff --git a/apps/ui/src/views/landing/LandingView.vue b/apps/ui/src/views/landing/LandingView.vue index d8acc74..c772fa5 100644 --- a/apps/ui/src/views/landing/LandingView.vue +++ b/apps/ui/src/views/landing/LandingView.vue @@ -1,30 +1,43 @@ -<script setup lang="ts"> -// Placeholder landing view. Implementation pending. -</script> +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<script setup lang="ts"></script> <template> - <main class="placeholder"> - <h1>SkyWalking Horizon UI</h1> - <p>Scaffold ready.</p> - </main> + <div class="placeholder"> + <h1>Horizon UI</h1> + <p>Shell ready. Routed views land in Phase 2 onward.</p> + </div> </template> <style scoped> .placeholder { - min-height: 100vh; - display: grid; - place-items: center; - background: var(--sw-bg-0); - color: var(--sw-fg-0); - font-family: var(--sw-sans); - text-align: center; - padding: 24px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 60vh; + gap: 8px; + color: var(--sw-fg-1); } .placeholder h1 { font-size: 22px; font-weight: 600; - letter-spacing: -0.01em; - margin-bottom: 8px; + letter-spacing: -0.02em; + color: var(--sw-fg-0); } .placeholder p { color: var(--sw-fg-2);
