467 lines
14 KiB
Vue
467 lines
14 KiB
Vue
<template>
|
|
<div class="instances-page">
|
|
<div class="page-header">
|
|
<h2>Instance Management</h2>
|
|
<button class="add-btn" @click="showAddModal = true">
|
|
<i class="fas fa-plus" aria-hidden="true"></i> Add Instance
|
|
</button>
|
|
</div>
|
|
<div class="instances-grid">
|
|
<div
|
|
v-for="instance in filteredInstances"
|
|
:key="instance.name"
|
|
class="instance-card"
|
|
@click="goToDetail(instance.name)"
|
|
>
|
|
<div class="card-header">
|
|
<h3 class="instance-name">{{ highlightText(instance.name, searchQuery) }}</h3>
|
|
<span :class="['status-badge', instance.status]">
|
|
{{ instance.status === 'running' ? 'Running' : 'Stopped' }}
|
|
</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="info-row" v-if="instance.webuiPort">
|
|
<span class="label">WebUI Port:</span>
|
|
<span class="value">{{ instance.webuiPort }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="label">Created At:</span>
|
|
<span class="value">{{ formatDate(instance.createdAt) }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="label">Last Active:</span>
|
|
<span class="value">{{ formatDate(instance.lastActive) }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer" @click.stop>
|
|
<button
|
|
v-if="instance.status !== 'running'"
|
|
class="action-btn start-btn"
|
|
@click="startInstance(instance.name)"
|
|
>
|
|
Start
|
|
</button>
|
|
<button
|
|
v-else
|
|
class="action-btn stop-btn"
|
|
@click="stopInstance(instance.name)"
|
|
>
|
|
Stop
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="filteredInstances.length === 0" class="empty-state">
|
|
<p>No instances available</p>
|
|
</div>
|
|
|
|
<div v-if="showAddModal" class="modal-overlay" @click="closeAddModal">
|
|
<div class="modal" @click.stop>
|
|
<h3>Add Instance</h3>
|
|
<form @submit.prevent="handleAddInstance" class="form">
|
|
<div class="form-group">
|
|
<label>Instance Name</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.name"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Authentication Method</label>
|
|
<select v-model="formData.auth_method" required>
|
|
<option value="token">Token</option>
|
|
<option value="oidc">OIDC</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Server Address</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.serverAddr"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Server Port</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.serverPort"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group" v-if="formData.auth_method === 'token'">
|
|
<label>Token</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.token"
|
|
required
|
|
>
|
|
</div>
|
|
<div v-if="formData.auth_method === 'oidc'">
|
|
<div class="form-group">
|
|
<label>Client ID</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.clientId"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Client Secret</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.clientSecret"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Audience</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.audience"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Token Endpoint URL</label>
|
|
<input
|
|
type="url"
|
|
v-model="formData.tokenEndpoint"
|
|
required
|
|
>
|
|
</div>
|
|
</div>
|
|
<div class="form-group checkbox-group">
|
|
<input
|
|
type="checkbox"
|
|
id="bootAtStart"
|
|
v-model="formData.bootAtStart"
|
|
>
|
|
<label for="bootAtStart">Boot at Start</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Run User</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.runUser"
|
|
placeholder="Default: root"
|
|
>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="cancel-btn" @click="closeAddModal">Cancel</button>
|
|
<button type="submit" class="submit-btn" :disabled="loading">
|
|
{{ loading ? 'Processing...' : 'Submit' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import { instanceApi } from '../api/index.js';
|
|
import { showNotification, formatDate, highlightText } from '../utils/functions.js';
|
|
|
|
export default {
|
|
name: 'Instances',
|
|
props: {
|
|
searchQuery: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
},
|
|
setup(props) {
|
|
const router = useRouter();
|
|
const instances = ref([]);
|
|
const showAddModal = ref(false);
|
|
const loading = ref(false);
|
|
const formData = ref({
|
|
name: '',
|
|
auth_method: 'token',
|
|
serverAddr: '',
|
|
serverPort: '',
|
|
token: '',
|
|
clientId: '',
|
|
clientSecret: '',
|
|
audience: '',
|
|
tokenEndpoint: '',
|
|
bootAtStart: false,
|
|
runUser: ''
|
|
});
|
|
|
|
const filteredInstances = computed(() => {
|
|
if (!props.searchQuery) return instances.value;
|
|
return instances.value.filter(instance =>
|
|
instance.name.toLowerCase().includes(props.searchQuery.toLowerCase())
|
|
);
|
|
});
|
|
|
|
const loadInstances = async () => {
|
|
try {
|
|
const result = await instanceApi.listInstances();
|
|
instances.value = result.data.map(instance => ({
|
|
...instance,
|
|
status: 'stopped'
|
|
}));
|
|
} catch (error) {
|
|
showNotification('Load instance list failed', 'error');
|
|
}
|
|
};
|
|
|
|
const startInstance = async (instanceName) => {
|
|
try {
|
|
await instanceApi.startInstance(instanceName);
|
|
showNotification('Instance started successfully', 'success');
|
|
await loadInstances();
|
|
} catch (error) {
|
|
showNotification(error.message || 'Start instance failed', 'error');
|
|
}
|
|
};
|
|
|
|
const stopInstance = async (instanceName) => {
|
|
try {
|
|
await instanceApi.stopInstance(instanceName);
|
|
showNotification('Instance stopped successfully', 'success');
|
|
await loadInstances();
|
|
} catch (error) {
|
|
showNotification(error.message || 'Stop instance failed', 'error');
|
|
}
|
|
};
|
|
|
|
const goToDetail = (instanceName) => {
|
|
router.push(`/instances/${instanceName}`);
|
|
};
|
|
|
|
const handleAddInstance = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const instanceInfo = {
|
|
name: formData.value.name,
|
|
serverAddr: formData.value.serverAddr,
|
|
serverPort: formData.value.serverPort,
|
|
auth_method: formData.value.auth_method
|
|
};
|
|
|
|
const additionalProperties = {};
|
|
if (formData.value.auth_method === 'token') {
|
|
additionalProperties.auth_token = formData.value.token;
|
|
} else if (formData.value.auth_method === 'oidc') {
|
|
additionalProperties.oidc_client_id = formData.value.clientId;
|
|
additionalProperties.oidc_client_secret = formData.value.clientSecret;
|
|
additionalProperties.oidc_audience = formData.value.audience;
|
|
additionalProperties.oidc_token_endpoint = formData.value.tokenEndpoint;
|
|
}
|
|
|
|
await instanceApi.createInstance(
|
|
instanceInfo,
|
|
formData.value.bootAtStart,
|
|
formData.value.runUser || 'root',
|
|
additionalProperties
|
|
);
|
|
|
|
showNotification('Instance created successfully', 'success');
|
|
closeAddModal();
|
|
await loadInstances();
|
|
} catch (error) {
|
|
showNotification(error.message || 'Failed to create instance', 'error');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const closeAddModal = () => {
|
|
showAddModal.value = false;
|
|
formData.value = {
|
|
name: '',
|
|
auth_method: 'token',
|
|
serverAddr: '',
|
|
serverPort: '',
|
|
token: '',
|
|
clientId: '',
|
|
clientSecret: '',
|
|
audience: '',
|
|
tokenEndpoint: '',
|
|
bootAtStart: false,
|
|
runUser: ''
|
|
};
|
|
};
|
|
|
|
onMounted(() => {
|
|
loadInstances();
|
|
});
|
|
|
|
return {
|
|
instances,
|
|
filteredInstances,
|
|
startInstance,
|
|
stopInstance,
|
|
goToDetail,
|
|
formatDate,
|
|
highlightText,
|
|
showAddModal,
|
|
loading,
|
|
formData,
|
|
handleAddInstance,
|
|
closeAddModal
|
|
};
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
@import '../styles/common.css';
|
|
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.instances-page {
|
|
padding: 24px;
|
|
}
|
|
|
|
.instances-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.instance-card {
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
background-color: var(--card-bg);
|
|
padding: 16px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.instance-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
.card-footer {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.instance-name {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--text-color);
|
|
margin: 0;
|
|
transition: color 0.3s;
|
|
}
|
|
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal {
|
|
background: var(--card-bg);
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
width: 90%;
|
|
max-width: 500px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.modal h3 {
|
|
margin-top: 0;
|
|
margin-bottom: 20px;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.form-group label {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group select {
|
|
padding: 10px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
background: var(--bg-color);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.form-group.checkbox-group {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.form-group.checkbox-group input {
|
|
width: auto;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 12px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.cancel-btn {
|
|
padding: 10px 20px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
background: transparent;
|
|
color: var(--text-color);
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.cancel-btn:hover {
|
|
background: var(--hover-bg);
|
|
}
|
|
|
|
.submit-btn {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: var(--primary-color);
|
|
color: white;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.submit-btn:hover {
|
|
background: var(--primary-hover);
|
|
}
|
|
|
|
.submit-btn:disabled {
|
|
background: var(--disabled-bg);
|
|
cursor: not-allowed;
|
|
}
|
|
</style>
|