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
30 changes: 29 additions & 1 deletion admin/ui/src/components/AddInstanceModal.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<div :class="$style.overlay" @click.self="$emit('close')">
<div
ref="modalRef"
:class="$style.modal"
role="dialog"
aria-labelledby="modal-title"
Expand Down Expand Up @@ -70,18 +71,20 @@
</template>

<script setup>
import { onMounted } from 'vue';
import { ref, onMounted, onUnmounted } from 'vue';
import BaseInput from './BaseInput.vue';
import BaseButton from './BaseButton.vue';
import { useInstanceStore } from '../stores/instances.js';
import { useInstanceForm } from '../composables/useInstanceForm.js';
import { useFocusTrap } from '../composables/useFocusTrap.js';

const props = defineProps({
instance: { type: Object, default: null },
});

const emit = defineEmits(['close', 'saved']);
const store = useInstanceStore();
const modalRef = ref(null);
const {
editing,
name,
Expand All @@ -94,6 +97,15 @@ const {
handleSubmit: submit,
} = useInstanceForm(store, props.instance);

useFocusTrap(modalRef);

function onKeydown(e) {
if (e.key === 'Escape') {
e.preventDefault();
emit('close');
}
}

async function handleSubmit() {
const ok = await submit();
if (ok) {
Expand All @@ -103,8 +115,13 @@ async function handleSubmit() {
}

onMounted(() => {
document.addEventListener('keydown', onKeydown);
document.querySelector('[data-testid="instance-name"]')?.focus();
});

onUnmounted(() => {
document.removeEventListener('keydown', onKeydown);
});
</script>

<style module>
Expand Down Expand Up @@ -167,4 +184,15 @@ onMounted(() => {
.spacer {
flex: 1;
}

@media (max-width: 768px) {
.modal {
margin: var(--space-4);
max-width: calc(100vw - var(--space-8));
}

.actions {
flex-wrap: wrap;
}
}
</style>
6 changes: 4 additions & 2 deletions admin/ui/src/components/BaseCard.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div :class="$style.card">
<div :class="$style.card" v-bind="$attrs">
<div v-if="$slots.header" :class="$style.header">
<slot name="header" />
</div>
Expand All @@ -9,7 +9,9 @@
</div>
</template>

<script setup></script>
<script setup>
defineOptions({ inheritAttrs: false });
</script>

<style module>
.card {
Expand Down
7 changes: 6 additions & 1 deletion admin/ui/src/components/ConfirmDialog.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<div :class="$style.overlay" @click.self="$emit('cancel')">
<div
ref="dialogRef"
:class="$style.dialog"
role="alertdialog"
:aria-labelledby="titleId"
Expand Down Expand Up @@ -30,8 +31,9 @@
</template>

<script setup>
import { onMounted, onUnmounted, useId } from 'vue';
import { ref, onMounted, onUnmounted, useId } from 'vue';
import BaseButton from './BaseButton.vue';
import { useFocusTrap } from '../composables/useFocusTrap.js';

defineProps({
title: { type: String, required: true },
Expand All @@ -41,10 +43,13 @@ defineProps({
});

const emit = defineEmits(['confirm', 'cancel']);
const dialogRef = ref(null);

const titleId = `confirm-title-${useId()}`;
const descId = `confirm-desc-${useId()}`;

useFocusTrap(dialogRef);

function onKeydown(e) {
if (e.key === 'Escape') {
e.preventDefault();
Expand Down
40 changes: 20 additions & 20 deletions admin/ui/src/components/InstanceCard.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
<template>
<BaseCard :class="$style.card" @click="$emit('click')">
<BaseCard
:class="$style.card"
tabindex="0"
role="link"
:aria-label="`View details for ${instance.name}`"
@click="$emit('click')"
@keydown.enter="onCardKeydown"
@keydown.space="onCardKeydown"
>
<div :class="$style.header">
<div :class="$style.titleRow">
<h3 :class="$style.name">{{ instance.name }}</h3>
<StatusIndicator
:status="isStale ? 'stale' : instance.status"
:label="statusLabel"
:status="instance.status"
:label="getStatusLabel(instance.status)"
/>
</div>
<div :class="$style.address">{{ instance.address }}</div>
Expand All @@ -17,7 +25,7 @@
</div>
<div v-if="instance.last_seen_at" :class="$style.metaItem">
<span :class="$style.metaLabel">Last seen</span>
<span :class="[$style.metaValue, isStale && $style.staleValue]">{{
<span :class="$style.metaValue">{{
formatTime(instance.last_seen_at)
}}</span>
</div>
Expand All @@ -42,26 +50,22 @@
</template>

<script setup>
import { computed } from 'vue';
import BaseCard from './BaseCard.vue';
import BaseButton from './BaseButton.vue';
import StatusIndicator from './StatusIndicator.vue';
import {
isInstanceStale,
formatTime,
getStatusLabel,
} from '../utils/instance.js';
import { formatTime, getStatusLabel } from '../utils/instance.js';

const props = defineProps({
defineProps({
instance: { type: Object, required: true },
});

defineEmits(['click', 'edit', 'delete']);
const emit = defineEmits(['click', 'edit', 'delete']);

const isStale = computed(() => isInstanceStale(props.instance));
const statusLabel = computed(() =>
getStatusLabel(props.instance.status, isStale.value),
);
function onCardKeydown(e) {
if (e.target.closest('button, a')) return;
e.preventDefault();
emit('click');
}
</script>

<style module>
Expand Down Expand Up @@ -122,10 +126,6 @@ const statusLabel = computed(() =>
color: var(--color-text-primary);
}

.staleValue {
color: var(--color-warning);
}

.actions {
display: flex;
gap: var(--space-2);
Expand Down
29 changes: 22 additions & 7 deletions admin/ui/src/components/InstanceTable.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div :class="$style.wrapper">
<table :class="$style.table">
<table :class="$style.table" aria-label="Registered instances">
<thead>
<tr>
<th :class="$style.th">Status</th>
Expand All @@ -16,12 +16,15 @@
v-for="inst in instances"
:key="inst.id"
:class="$style.row"
tabindex="0"
role="link"
:aria-label="`View details for ${inst.name}`"
@click="$emit('click', inst)"
@keydown.enter="onRowKeydown($event, inst)"
@keydown.space="onRowKeydown($event, inst)"
>
<td :class="$style.td">
<StatusIndicator
:status="isInstanceStale(inst) ? 'stale' : inst.status"
/>
<StatusIndicator :status="inst.status" />
</td>
<td :class="[$style.td, $style.name]">{{ inst.name }}</td>
<td :class="[$style.td, $style.mono]">{{ inst.address }}</td>
Expand Down Expand Up @@ -52,13 +55,19 @@
<script setup>
import StatusIndicator from './StatusIndicator.vue';
import BaseButton from './BaseButton.vue';
import { isInstanceStale, formatTime } from '../utils/instance.js';
import { formatTime } from '../utils/instance.js';

defineProps({
instances: { type: Array, required: true },
});

defineEmits(['click', 'edit', 'delete']);
const emit = defineEmits(['click', 'edit', 'delete']);

function onRowKeydown(e, inst) {
if (e.target.closest('button, a')) return;
e.preventDefault();
emit('click', inst);
}
</script>

<style module>
Expand Down Expand Up @@ -87,10 +96,16 @@ defineEmits(['click', 'edit', 'delete']);
cursor: pointer;
}

.row:hover {
.row:hover,
.row:focus-visible {
background-color: var(--color-bg-primary);
}

.row:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: -2px;
}

.td {
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--color-border-light);
Expand Down
75 changes: 75 additions & 0 deletions admin/ui/src/components/LoadingSpinner.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<template>
<div :class="[$style.spinner, $style[size]]" role="status">
<svg
:class="$style.icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
>
<path d="M12 2a10 10 0 0 1 10 10" />
</svg>
<span :class="$style.srOnly">{{ label }}</span>
</div>
</template>

<script setup>
defineProps({
size: {
type: String,
default: 'md',
validator: (v) => ['sm', 'md', 'lg'].includes(v),
},
label: {
type: String,
default: 'Loading...',
},
});
</script>

<style module>
.spinner {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--color-text-tertiary);
}

.icon {
animation: spin 0.8s linear infinite;
}

.sm .icon {
width: 16px;
height: 16px;
}

.md .icon {
width: 24px;
height: 24px;
}

.lg .icon {
width: 32px;
height: 32px;
}

.srOnly {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
10 changes: 8 additions & 2 deletions admin/ui/src/components/OverviewTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@

<!-- Latency distribution chart -->
<div :class="$style.chartSection">
<h3 :class="$style.chartTitle">Latency Over Time</h3>
<div :class="$style.chartContainer">
<h3 id="latency-chart-title" :class="$style.chartTitle">
Latency Over Time
</h3>
<div
:class="$style.chartContainer"
role="img"
aria-labelledby="latency-chart-title"
>
<VChart
v-if="hasSeries"
:option="latencyChartOption"
Expand Down
13 changes: 1 addition & 12 deletions admin/ui/src/components/StatusIndicator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ const props = defineProps({
status: {
type: String,
default: 'unknown',
validator: (v) =>
['healthy', 'unreachable', 'unknown', 'stale'].includes(v),
validator: (v) => ['healthy', 'unreachable', 'unknown'].includes(v),
},
label: {
type: String,
Expand All @@ -29,7 +28,6 @@ const statusLabels = {
healthy: 'Healthy',
unreachable: 'Unreachable',
unknown: 'Unknown',
stale: 'Stale',
};

const computedAriaLabel = computed(() => {
Expand Down Expand Up @@ -83,13 +81,4 @@ const computedAriaLabel = computed(() => {
.unknown .label {
color: var(--color-text-tertiary);
}

.stale .dot {
background-color: var(--color-warning);
box-shadow: 0 0 0 3px var(--color-warning-bg);
}

.stale .label {
color: var(--color-warning);
}
</style>
Loading
Loading