Add basic support for agent

This commit is contained in:
cuigh
2021-12-17 20:13:58 +08:00
parent 94127504ff
commit cb2cb4ab86
23 changed files with 402 additions and 197 deletions

View File

@@ -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 })
}

View File

@@ -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')
}

View File

@@ -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>

View File

@@ -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 });

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'),
},
{

View File

@@ -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 })
}