Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply new mockup #47

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
Binary file modified ping-viewer-next-frontend/bun.lockb
Binary file not shown.
18 changes: 9 additions & 9 deletions ping-viewer-next-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
"core-js": "^3.39.0",
"roboto-fontface": "^0.10.0",
"vue": "3.5.8",
"vuetify": "^3.7.4"
"vuetify": "^3.7.6"
},
"devDependencies": {
"@biomejs/biome": "1.9.2",
"@vitejs/plugin-vue": "^5.1.5",
"@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-standard": "^17.1.0",
Expand All @@ -25,18 +25,18 @@
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.6.0",
"eslint-plugin-vue": "^9.31.0",
"pinia": "^2.2.6",
"eslint-plugin-vue": "^9.32.0",
"pinia": "^2.3.0",
"postcss": "^8.4.49",
"sass": "1.77.6",
"tailwindcss": "^3.4.14",
"tailwindcss": "^3.4.17",
"unplugin-auto-import": "^0.17.8",
"unplugin-fonts": "^1.1.1",
"unplugin-vue-components": "^0.27.4",
"unplugin-vue-router": "^0.10.8",
"unplugin-fonts": "^1.3.1",
"unplugin-vue-components": "^0.27.5",
"unplugin-vue-router": "^0.10.9",
"vite": "^5.4.11",
"vite-plugin-vue-layouts": "^0.11.0",
"vite-plugin-vuetify": "^2.0.4",
"vue-router": "^4.4.5"
"vue-router": "^4.5.0"
}
}
1,100 changes: 911 additions & 189 deletions ping-viewer-next-frontend/src/App.vue

Large diffs are not rendered by default.

345 changes: 345 additions & 0 deletions ping-viewer-next-frontend/src/components/ConnectionManager.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
<template>
<div class="connection-manager">
<div class="config-menu" :class="{ 'glass disable-hover': glass }" v-show="isOpen">
<div :class="['menu-content', { 'glass-inner disable-hover': glass }]">
<!-- Header Section -->
<div class="menu-header d-flex justify-space-between align-center mb-4">
<div class="text-h6">Device Management</div>
<div class="d-flex gap-2">
<v-btn color="primary" size="small" @click="autoCreateDevices" :loading="isAutoCreating">
<v-icon start>mdi-plus-network</v-icon>
Auto Create
</v-btn>
<v-btn color="primary" size="small" @click="refreshDevices" :loading="isRefreshing">
<v-icon>mdi-refresh</v-icon>
</v-btn>
</div>
</div>

<!-- Device List -->
<div class="device-list mb-4">
<div v-if="isLoading" class="d-flex justify-center my-4">
<v-progress-circular indeterminate />
</div>

<div v-else-if="devices.length === 0" class="text-center pa-4 text-medium-emphasis">
<v-icon size="48" class="mb-2">mdi-devices</v-icon>
<div>No devices found.</div>
<div class="text-caption">Try clicking 'Auto Create' to discover devices.</div>
</div>

<v-list v-else :class="{ 'glass-inner': glass }" density="compact">
<v-list-item v-for="device in devices" :key="device.id" :value="device" class="mb-2"
:class="{ 'glass-inner': glass }">
<template v-slot:prepend>
<v-icon :icon="device.device_type === 'Ping360' ? 'mdi-radar' : 'mdi-altimeter'" />
</template>

<v-list-item-title>{{ device.device_type }}</v-list-item-title>
<v-list-item-subtitle class="text-truncate">{{ device.id }}</v-list-item-subtitle>

<template v-slot:append>
<div class="d-flex align-center">
<v-chip :color="getStatusColor(device.status)" size="small" class="mr-2">
{{ device.status }}
</v-chip>
<v-btn color="primary" size="small" @click="selectDevice(device)">
Open
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
</div>

<!-- Manual Creation Section -->
<v-expand-transition>
<div v-if="showManualCreate" :class="{ 'glass-inner': glass }" class="pa-4 rounded">
<v-form @submit.prevent="createDevice">
<v-select v-model="newDevice.device_selection" :items="deviceTypes" label="Device Type" class="mb-4" />
<v-select v-model="newDevice.connectionType" :items="connectionTypes" label="Connection Type"
class="mb-4" />

<template v-if="newDevice.connectionType === 'UdpStream'">
<v-text-field v-model="newDevice.udp.ip" label="IP Address" class="mb-4"
:rules="[v => !!v || 'IP is required']" />
<v-text-field v-model.number="newDevice.udp.port" type="number" label="Port" class="mb-4"
:rules="[v => !!v || 'Port is required']" />
</template>

<template v-else-if="newDevice.connectionType === 'SerialStream'">
<v-text-field v-model="newDevice.serial.path" label="Serial Path" class="mb-4"
:rules="[v => !!v || 'Path is required']" />
<v-text-field v-model.number="newDevice.serial.baudrate" type="number" label="Baudrate" class="mb-4"
:rules="[v => !!v || 'Baudrate is required']" />
</template>

<div class="d-flex justify-end gap-2">
<v-btn color="error" variant="text" @click="showManualCreate = false">Cancel</v-btn>
<v-btn color="primary" :loading="isCreating" type="submit">Create</v-btn>
</div>
</v-form>
</div>
</v-expand-transition>

<!-- Footer Actions -->
<div class="menu-actions mt-4">
<v-btn block :class="{ 'glass-inner': glass }" @click="showManualCreate = !showManualCreate">
<v-icon start>{{ showManualCreate ? 'mdi-minus' : 'mdi-plus' }}</v-icon>
{{ showManualCreate ? 'Cancel Manual Creation' : 'Manual Create' }}
</v-btn>
</div>
</div>
</div>
</div>
</template>

<script setup>
import { onMounted, onUnmounted, ref } from 'vue';

const props = defineProps({
serverUrl: {
type: String,
required: true,
},
glass: {
type: Boolean,
default: false,
},
isOpen: {
type: Boolean,
required: true,
},
});

const emit = defineEmits(['update:isOpen', 'select-device']);

// State
const devices = ref([]);
const isLoading = ref(false);
const isRefreshing = ref(false);
const isAutoCreating = ref(false);
const isCreating = ref(false);
const showManualCreate = ref(false);
const error = ref(null);

// Form state
const newDevice = ref({
device_selection: 'Auto',
connectionType: 'UdpStream',
udp: {
ip: 'blueos.local',
port: 12345,
},
serial: {
path: '/dev/ttyUSB0',
baudrate: 2500000,
},
});

// Constants
const deviceTypes = [
{ title: 'Auto Detect', value: 'Auto' },
{ title: 'Ping1D', value: 'Ping1D' },
{ title: 'Ping360', value: 'Ping360' },
];

const connectionTypes = [
{ title: 'UDP', value: 'UdpStream' },
{ title: 'Serial', value: 'SerialStream' },
];

// Utility functions
const getStatusColor = (status) => {
switch (status) {
case 'ContinuousMode':
return 'success';
case 'Running':
return 'info';
case 'Error':
return 'error';
default:
return 'warning';
}
};

// API functions
const fetchDevices = async () => {
try {
const response = await fetch(`${props.serverUrl}/device_manager/request`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
command: 'List',
module: 'DeviceManager',
}),
});

if (!response.ok) throw new Error('Failed to fetch devices');

const data = await response.json();
devices.value = data.DeviceInfo || [];
error.value = null;
} catch (err) {
console.error('Error fetching devices:', err);
error.value = `Failed to fetch devices: ${err.message}`;
}
};

const refreshDevices = async () => {
isRefreshing.value = true;
await fetchDevices();
isRefreshing.value = false;
};

const autoCreateDevices = async () => {
isAutoCreating.value = true;
error.value = null;

try {
const response = await fetch(`${props.serverUrl}/device_manager/request`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
command: 'AutoCreate',
module: 'DeviceManager',
}),
});

if (!response.ok) throw new Error('Failed to auto-create devices');
await refreshDevices();
} catch (err) {
console.error('Error auto-creating devices:', err);
error.value = `Failed to auto-create devices: ${err.message}`;
} finally {
isAutoCreating.value = false;
}
};

const createDevice = async () => {
isCreating.value = true;
error.value = null;

try {
const source =
newDevice.value.connectionType === 'UdpStream'
? {
UdpStream: {
ip: newDevice.value.udp.ip,
port: newDevice.value.udp.port,
},
}
: {
SerialStream: {
path: newDevice.value.serial.path,
baudrate: newDevice.value.serial.baudrate,
},
};

const response = await fetch(`${props.serverUrl}/device_manager/request`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
command: 'Create',
module: 'DeviceManager',
payload: {
device_selection: newDevice.value.device_selection,
source,
},
}),
});

if (!response.ok) throw new Error('Failed to create device');

await refreshDevices();
showManualCreate.value = false;
} catch (err) {
console.error('Error creating device:', err);
error.value = `Failed to create device: ${err.message}`;
} finally {
isCreating.value = false;
}
};

const selectDevice = (device) => {
emit('select-device', device);
emit('update:isOpen', false);
};

// Lifecycle hooks
let refreshInterval;

onMounted(() => {
isLoading.value = true;
fetchDevices().finally(() => {
isLoading.value = false;
});
refreshInterval = setInterval(fetchDevices, 5000);
});

onUnmounted(() => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
</script>

<style scoped>
.connection-manager {
transition: all 0.3s ease;
transform-origin: top left;
animation: slideIn 0.3s ease;
}

@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}

to {
opacity: 1;
transform: translateX(0);
}
}

/* Responsive adjustments */
@media (max-width: 600px) {
.connection-menu-wrapper .config-menu {
width: calc(100vw - var(--button-size) - var(--button-gap) * 2);
max-width: 400px;
}
}

.config-menu {
width: 400px;
max-width: calc(100vw - var(--button-size));
border-radius: var(--border-radius);
overflow: hidden;
background: rgb(var(--v-theme-background));
}

.menu-content {
padding: 1rem;
}

.device-list {
max-height: 50vh;
overflow-y: auto;
}

/* :deep(.v-list) {
background: transparent;
} */

.menu-actions {
border-top: 1px solid rgba(var(--v-border-color), 0.12);
padding-top: 1rem;
}
</style>
Loading