mirror of
https://github.com/cuigh/swirl
synced 2025-06-26 18:16:50 +00:00
Add basic support for agent
This commit is contained in:
@@ -36,6 +36,7 @@ export interface Container {
|
||||
}
|
||||
|
||||
export interface SearchArgs {
|
||||
node?: string;
|
||||
name?: string;
|
||||
status?: string;
|
||||
pageIndex: number;
|
||||
@@ -53,21 +54,22 @@ export interface FindResult {
|
||||
}
|
||||
|
||||
export interface FetchLogsArgs {
|
||||
node: string;
|
||||
id: string;
|
||||
lines: number;
|
||||
timestamps: boolean;
|
||||
}
|
||||
|
||||
export class ContainerApi {
|
||||
find(id: string) {
|
||||
return ajax.get<FindResult>('/container/find', { id })
|
||||
find(node: string, id: string) {
|
||||
return ajax.get<FindResult>('/container/find', { node, id })
|
||||
}
|
||||
|
||||
search(args: SearchArgs) {
|
||||
return ajax.get<SearchResult>('/container/search', args)
|
||||
}
|
||||
|
||||
delete(id: string, name: string) {
|
||||
delete(node: string, id: string, name: string) {
|
||||
return ajax.post<Result<Object>>('/container/delete', { id, name })
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@ export class NodeApi {
|
||||
return ajax.get<FindResult>('/node/find', { id })
|
||||
}
|
||||
|
||||
list() {
|
||||
return ajax.get<Node[]>('/node/list')
|
||||
}
|
||||
|
||||
search() {
|
||||
return ajax.get<Node[]>('/node/search')
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
<script setup lang="ts">
|
||||
import { NA } from "naive-ui";
|
||||
import { RouterLink } from "vue-router";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
})
|
||||
interface Props {
|
||||
url: RouteLocationRaw
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
@@ -60,6 +60,9 @@ const props = defineProps({
|
||||
type: String as PropType<'task' | 'container' | 'service'>,
|
||||
required: true,
|
||||
},
|
||||
node: {
|
||||
type: String,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -88,7 +91,7 @@ async function fetchData() {
|
||||
var r: Result<Logs>;
|
||||
switch (props.type) {
|
||||
case 'container':
|
||||
r = await containerApi.fetchLogs({ id: props.id, lines: filters.lines, timestamps: filters.timestamps });
|
||||
r = await containerApi.fetchLogs({ node: props.node || '', id: props.id, lines: filters.lines, timestamps: filters.timestamps });
|
||||
break
|
||||
case 'task':
|
||||
r = await taskApi.fetchLogs({ id: props.id, lines: filters.lines, timestamps: filters.timestamps });
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
<x-page-header />
|
||||
<n-space class="page-body" vertical :size="12">
|
||||
<n-space :size="12">
|
||||
<n-select
|
||||
size="small"
|
||||
:placeholder="t('objects.node')"
|
||||
v-model:value="filter.node"
|
||||
:options="nodes"
|
||||
style="width: 200px"
|
||||
v-if="nodes && nodes.length"
|
||||
/>
|
||||
<n-input size="small" v-model:value="filter.name" :placeholder="t('fields.name')" clearable />
|
||||
<n-button size="small" type="primary" @click="() => fetchData()">{{ t('buttons.search') }}</n-button>
|
||||
</n-space>
|
||||
@@ -21,33 +29,40 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from "vue";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import {
|
||||
NSpace,
|
||||
NButton,
|
||||
NDataTable,
|
||||
NInput,
|
||||
NSelect,
|
||||
} from "naive-ui";
|
||||
import XPageHeader from "@/components/PageHeader.vue";
|
||||
import containerApi from "@/api/container";
|
||||
import type { Container } from "@/api/container";
|
||||
import nodeApi from "@/api/node";
|
||||
import { useDataTable } from "@/utils/data-table";
|
||||
import { renderButton, renderLink, renderTag } from "@/utils/render";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const filter = reactive({
|
||||
name: "",
|
||||
node: '',
|
||||
name: '',
|
||||
});
|
||||
const nodes: any = ref([])
|
||||
const columns = [
|
||||
{
|
||||
title: t('fields.name'),
|
||||
key: "name",
|
||||
fixed: "left" as const,
|
||||
render: (c: Container) => renderLink(`/local/containers/${c.id}`, c.name),
|
||||
render: (c: Container) => {
|
||||
const node = c.labels?.find(l => l.name === 'com.docker.swarm.node.id')
|
||||
return renderLink({ name: 'container_detail', params: { id: c.id, node: node?.value || '@' } }, c.name)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('objects.image'),
|
||||
title: t('objects.image'),
|
||||
key: "image",
|
||||
},
|
||||
{
|
||||
@@ -65,14 +80,24 @@ const columns = [
|
||||
title: t('fields.actions'),
|
||||
key: "actions",
|
||||
render(i: Container, index: number) {
|
||||
return renderButton('error', t('buttons.delete'), () => deleteContainer(i.id, index), t('prompts.delete'))
|
||||
return renderButton('error', t('buttons.delete'), () => deleteContainer(i, index), t('prompts.delete'))
|
||||
},
|
||||
},
|
||||
];
|
||||
const { state, pagination, fetchData, changePageSize } = useDataTable(containerApi.search, filter)
|
||||
const { state, pagination, fetchData, changePageSize } = useDataTable(containerApi.search, filter, false)
|
||||
|
||||
async function deleteContainer(id: string, index: number) {
|
||||
await containerApi.delete(id, "");
|
||||
async function deleteContainer(c: Container, index: number) {
|
||||
const node = c.labels?.find(l => l.name === 'com.docker.swarm.node.id')
|
||||
await containerApi.delete(node?.value || '', c.id, '');
|
||||
state.data.splice(index, 1)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const r = await nodeApi.list()
|
||||
nodes.value = r.data?.map(n => ({ label: n.name, value: n.id }))
|
||||
if (r.data?.length) {
|
||||
filter.node = r.data[0].id
|
||||
}
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
@@ -52,10 +52,10 @@
|
||||
<x-code :code="raw" language="json" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="logs" :tab="t('fields.logs')" display-directive="show:lazy">
|
||||
<x-logs type="container" :id="model.id"></x-logs>
|
||||
<x-logs type="container" :node="node" :id="model.id"></x-logs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="exec" :tab="t('fields.execute')" display-directive="show:lazy">
|
||||
<execute :container-id="model.id"></execute>
|
||||
<execute :node="node" :id="model.id"></execute>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
@@ -88,10 +88,11 @@ const { t } = useI18n()
|
||||
const route = useRoute();
|
||||
const model = ref({} as Container);
|
||||
const raw = ref('');
|
||||
const node = route.params.node as string || '';
|
||||
|
||||
async function fetchData() {
|
||||
const id = route.params.id as string;
|
||||
let r = await containerApi.find(id);
|
||||
let r = await containerApi.find(node, id);
|
||||
model.value = r.data?.container as Container;
|
||||
raw.value = r.data?.raw as string;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const props = defineProps({
|
||||
containerId: {
|
||||
node: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
@@ -47,7 +51,7 @@ function connect() {
|
||||
let protocol = (location.protocol === "https:") ? "wss://" : "ws://";
|
||||
let host = import.meta.env.DEV ? 'localhost:8002' : location.host;
|
||||
let cmd = encodeURIComponent(command.value)
|
||||
socket = new WebSocket(`${protocol}${host}/api/container/connect?id=${props.containerId}&cmd=${cmd}`);
|
||||
socket = new WebSocket(`${protocol}${host}/api/container/connect?node=${props.node}&id=${props.id}&cmd=${cmd}`);
|
||||
socket.onopen = () => {
|
||||
const fit = new FitAddon();
|
||||
term = new Terminal({ fontSize: 14, cursorBlink: true });
|
||||
@@ -57,9 +61,9 @@ function connect() {
|
||||
fit.fit();
|
||||
term.focus();
|
||||
};
|
||||
socket.onclose = () => {
|
||||
console.log('close socket')
|
||||
};
|
||||
// socket.onclose = () => {
|
||||
// console.log('close socket')
|
||||
// };
|
||||
socket.onerror = (e) => {
|
||||
console.log('socket error: ' + e)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<n-input :placeholder="t('objects.image')" v-model:value="model.image" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :label="t('fields.mode')" path="mode">
|
||||
<n-radio-group v-model:value="model.mode">
|
||||
<n-radio-group v-model:value="model.mode" :disabled="Boolean(model.id)">
|
||||
<n-radio key="replicated" value="replicated">Replicated</n-radio>
|
||||
<n-radio key="global" value="global">Global</n-radio>
|
||||
<n-radio key="replicated-job" value="replicated-job">Replicated Job</n-radio>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<x-anchor :url="`/swarm/services/${model.serviceName}`">{{ model.serviceName }}</x-anchor>
|
||||
</x-description-item>
|
||||
<x-description-item :label="t('objects.container')" :span="2">
|
||||
<x-anchor :url="`/local/containers/${model.containerId}`">{{ model.containerId }}</x-anchor>
|
||||
<x-anchor :url="`/local/containers/${model.nodeId}/${model.containerId}`">{{ model.containerId }}</x-anchor>
|
||||
</x-description-item>
|
||||
<x-description-item :label="t('objects.node')" :span="2">
|
||||
<x-anchor :url="`/swarm/nodes/${model.nodeId}`">{{ model.nodeId }}</x-anchor>
|
||||
|
||||
@@ -213,7 +213,7 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
{
|
||||
name: "container_detail",
|
||||
path: "/local/containers/:id",
|
||||
path: "/local/containers/:node/:id",
|
||||
component: () => import('../pages/container/View.vue'),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { h } from "vue";
|
||||
import Anchor from "../components/Anchor.vue";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
import { NButton, NPopconfirm, NSpace, NTag, NTime } from "naive-ui";
|
||||
import Anchor from "../components/Anchor.vue";
|
||||
|
||||
/**
|
||||
* Format duration
|
||||
@@ -59,7 +60,7 @@ export function formatSize(value: number) {
|
||||
return size.toFixed(2) + ' ' + units[index];
|
||||
}
|
||||
|
||||
export function renderLink(url: string, text: string) {
|
||||
export function renderLink(url: RouteLocationRaw, text: string) {
|
||||
return h(Anchor, { url }, { default: () => text })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user