mirror of
https://github.com/cuigh/swirl
synced 2024-12-28 14:51:57 +00:00
Add chart detail page
This commit is contained in:
parent
118b5173ab
commit
bd7b8364d9
@ -17,7 +17,7 @@ RUN CGO_ENABLED=0 go build -ldflags "-s -w"
|
|||||||
FROM alpine
|
FROM alpine
|
||||||
LABEL maintainer="cuigh <noname@live.com>"
|
LABEL maintainer="cuigh <noname@live.com>"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk add --no-cache ca-certificates
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
COPY --from=golang /app/swirl .
|
COPY --from=golang /app/swirl .
|
||||||
COPY --from=golang /app/config config/
|
COPY --from=golang /app/config config/
|
||||||
EXPOSE 8001
|
EXPOSE 8001
|
||||||
|
@ -91,7 +91,7 @@ func chartDelete(b biz.ChartBiz) web.HandlerFunc {
|
|||||||
|
|
||||||
func chartSave(b biz.ChartBiz) web.HandlerFunc {
|
func chartSave(b biz.ChartBiz) web.HandlerFunc {
|
||||||
return func(ctx web.Context) error {
|
return func(ctx web.Context) error {
|
||||||
r := &model.Chart{}
|
r := &biz.Chart{}
|
||||||
err := ctx.Bind(r, true)
|
err := ctx.Bind(r, true)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if r.ID == "" {
|
if r.ID == "" {
|
||||||
|
44
biz/chart.go
44
biz/chart.go
@ -17,10 +17,10 @@ import (
|
|||||||
type ChartBiz interface {
|
type ChartBiz interface {
|
||||||
Search(title, dashboard string, pageIndex, pageSize int) (charts []*Chart, total int, err error)
|
Search(title, dashboard string, pageIndex, pageSize int) (charts []*Chart, total int, err error)
|
||||||
Delete(id, title string, user web.User) (err error)
|
Delete(id, title string, user web.User) (err error)
|
||||||
Find(id string) (chart *model.Chart, err error)
|
Find(id string) (chart *Chart, err error)
|
||||||
Batch(ids ...string) (charts []*model.Chart, err error)
|
Batch(ids ...string) (charts []*model.Chart, err error)
|
||||||
Create(chart *model.Chart, user web.User) (err error)
|
Create(chart *Chart, user web.User) (err error)
|
||||||
Update(chart *model.Chart, user web.User) (err error)
|
Update(chart *Chart, user web.User) (err error)
|
||||||
FetchData(key string, ids []string, period time.Duration) (data.Map, error)
|
FetchData(key string, ids []string, period time.Duration) (data.Map, error)
|
||||||
FindDashboard(name, key string) (dashboard *Dashboard, err error)
|
FindDashboard(name, key string) (dashboard *Dashboard, err error)
|
||||||
UpdateDashboard(dashboard *model.Dashboard, user web.User) (err error)
|
UpdateDashboard(dashboard *model.Dashboard, user web.User) (err error)
|
||||||
@ -59,13 +59,19 @@ func (b *chartBiz) Search(title, dashboard string, pageIndex, pageSize int) (cha
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *chartBiz) Create(chart *model.Chart, user web.User) (err error) {
|
func (b *chartBiz) Create(chart *Chart, user web.User) (err error) {
|
||||||
chart.ID = createId()
|
c := &model.Chart{
|
||||||
chart.CreatedAt = time.Now()
|
ID: createId(),
|
||||||
chart.UpdatedAt = chart.CreatedAt
|
CreatedAt: time.Now(),
|
||||||
err = b.d.ChartCreate(context.TODO(), chart)
|
}
|
||||||
|
c.UpdatedAt = c.CreatedAt
|
||||||
|
if err = copier.CopyWithOption(c, chart, copier.Option{IgnoreEmpty: true, DeepCopy: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = b.d.ChartCreate(context.TODO(), c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
b.eb.CreateChart(EventActionCreate, chart.ID, chart.Title, user)
|
b.eb.CreateChart(EventActionCreate, c.ID, c.Title, user)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -78,8 +84,12 @@ func (b *chartBiz) Delete(id, title string, user web.User) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *chartBiz) Find(id string) (chart *model.Chart, err error) {
|
func (b *chartBiz) Find(id string) (chart *Chart, err error) {
|
||||||
chart, err = b.d.ChartGet(context.TODO(), id)
|
var c *model.Chart
|
||||||
|
c, err = b.d.ChartGet(context.TODO(), id)
|
||||||
|
if err == nil {
|
||||||
|
chart = newChart(c)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,9 +98,15 @@ func (b *chartBiz) Batch(ids ...string) (charts []*model.Chart, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *chartBiz) Update(chart *model.Chart, user web.User) (err error) {
|
func (b *chartBiz) Update(chart *Chart, user web.User) (err error) {
|
||||||
chart.UpdatedAt = time.Now()
|
c := &model.Chart{
|
||||||
err = b.d.ChartUpdate(context.TODO(), chart)
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if err = copier.CopyWithOption(c, chart, copier.Option{IgnoreEmpty: true, DeepCopy: true}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = b.d.ChartUpdate(context.TODO(), c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
b.eb.CreateChart(EventActionUpdate, chart.ID, chart.Title, user)
|
b.eb.CreateChart(EventActionUpdate, chart.ID, chart.Title, user)
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,21 @@ func (d *Dao) ChartBatch(ctx context.Context, names ...string) (charts []*model.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dao) ChartUpdate(ctx context.Context, chart *model.Chart) (err error) {
|
func (d *Dao) ChartUpdate(ctx context.Context, chart *model.Chart) (err error) {
|
||||||
return d.update(ctx, Chart, chart.ID, chart)
|
update := bson.M{
|
||||||
|
"$set": bson.M{
|
||||||
|
"title": chart.Title,
|
||||||
|
"desc": chart.Description,
|
||||||
|
"width": chart.Width,
|
||||||
|
"height": chart.Height,
|
||||||
|
"unit": chart.Unit,
|
||||||
|
"dashboard": chart.Dashboard,
|
||||||
|
"type": chart.Type,
|
||||||
|
"margin": chart.Margin,
|
||||||
|
"metrics": chart.Metrics,
|
||||||
|
"updated_at": chart.UpdatedAt,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return d.update(ctx, Chart, chart.ID, update)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dao) ChartDelete(ctx context.Context, id string) (err error) {
|
func (d *Dao) ChartDelete(ctx context.Context, id string) (err error) {
|
||||||
@ -69,5 +83,5 @@ func (d *Dao) DashboardUpdate(ctx context.Context, dashboard *model.Dashboard) (
|
|||||||
update := bson.M{
|
update := bson.M{
|
||||||
"$set": dashboard,
|
"$set": dashboard,
|
||||||
}
|
}
|
||||||
return d.update(ctx, Dashboard, dashboard.ID(), update)
|
return d.upsert(ctx, Dashboard, dashboard.ID(), update)
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ type EventListArgs struct {
|
|||||||
type Chart struct {
|
type Chart struct {
|
||||||
ID string `json:"id" bson:"_id"` // unique, the name of build-in charts has '$' prefix.
|
ID string `json:"id" bson:"_id"` // unique, the name of build-in charts has '$' prefix.
|
||||||
Title string `json:"title" valid:"required"`
|
Title string `json:"title" valid:"required"`
|
||||||
Description string `json:"desc"`
|
Description string `json:"desc" bson:"desc"`
|
||||||
Metrics []ChartMetric `json:"metrics" valid:"required"`
|
Metrics []ChartMetric `json:"metrics" valid:"required"`
|
||||||
Dashboard string `json:"dashboard"` // home/service/task...
|
Dashboard string `json:"dashboard"` // home/service/task...
|
||||||
Type string `json:"type"` // pie/line...
|
Type string `json:"type"` // pie/line...
|
||||||
|
@ -187,6 +187,8 @@ export default {
|
|||||||
"right": "Right",
|
"right": "Right",
|
||||||
"top": "Top",
|
"top": "Top",
|
||||||
"bottom": "Bottom",
|
"bottom": "Bottom",
|
||||||
|
"legend": "Legend",
|
||||||
|
"query": "Query",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"security": "Security",
|
"security": "Security",
|
||||||
"authentication": "Authentication",
|
"authentication": "Authentication",
|
||||||
@ -304,6 +306,7 @@ export default {
|
|||||||
"chart_list": "Charts",
|
"chart_list": "Charts",
|
||||||
"chart_new": "New chart",
|
"chart_new": "New chart",
|
||||||
"chart_edit": "Edit chart",
|
"chart_edit": "Edit chart",
|
||||||
|
"chart_detail": "Chart detail",
|
||||||
"event_list": "Events",
|
"event_list": "Events",
|
||||||
"setting": "Settings",
|
"setting": "Settings",
|
||||||
"403": "403 Forbidden",
|
"403": "403 Forbidden",
|
||||||
|
@ -187,6 +187,8 @@ export default {
|
|||||||
"right": "右",
|
"right": "右",
|
||||||
"top": "上",
|
"top": "上",
|
||||||
"bottom": "下",
|
"bottom": "下",
|
||||||
|
"legend": "图例",
|
||||||
|
"query": "查询",
|
||||||
"enabled": "启用",
|
"enabled": "启用",
|
||||||
"security": "安全",
|
"security": "安全",
|
||||||
"authentication": "认证",
|
"authentication": "认证",
|
||||||
@ -304,6 +306,7 @@ export default {
|
|||||||
"chart_list": "图表列表",
|
"chart_list": "图表列表",
|
||||||
"chart_new": "新建图表",
|
"chart_new": "新建图表",
|
||||||
"chart_edit": "编辑图表",
|
"chart_edit": "编辑图表",
|
||||||
|
"chart_detail": "图表详情",
|
||||||
"event_list": "事件列表",
|
"event_list": "事件列表",
|
||||||
"setting": "设置",
|
"setting": "设置",
|
||||||
"403": "403 禁止访问",
|
"403": "403 禁止访问",
|
||||||
|
@ -140,7 +140,7 @@ import chartApi from "@/api/chart";
|
|||||||
import type { Chart } from "@/api/chart";
|
import type { Chart } from "@/api/chart";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { router } from "@/router/router";
|
import { router } from "@/router/router";
|
||||||
import { useForm, requiredRule } from "@/utils/form";
|
import { useForm, requiredRule, customRule } from "@/utils/form";
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@ -212,6 +212,9 @@ const dashboards = [
|
|||||||
]
|
]
|
||||||
const rules: any = {
|
const rules: any = {
|
||||||
title: requiredRule(),
|
title: requiredRule(),
|
||||||
|
metrics: customRule((rule: any, value: any[]) => {
|
||||||
|
return !!value && value.length > 0
|
||||||
|
}, t('tips.required_rule')),
|
||||||
};
|
};
|
||||||
const form = ref();
|
const form = ref();
|
||||||
const { submit, submiting } = useForm(form, () => chartApi.save(model.value), () => {
|
const { submit, submiting } = useForm(form, () => chartApi.save(model.value), () => {
|
||||||
|
@ -84,7 +84,7 @@ const columns = [
|
|||||||
title: t('fields.title'),
|
title: t('fields.title'),
|
||||||
key: "title",
|
key: "title",
|
||||||
fixed: "left" as const,
|
fixed: "left" as const,
|
||||||
render: (c: Chart) => renderLink(`/system/charts/${c.id}/edit`, c.title),
|
render: (c: Chart) => renderLink(`/system/charts/${c.id}`, c.title),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('fields.type'),
|
title: t('fields.type'),
|
||||||
|
108
ui/src/pages/chart/View.vue
Normal file
108
ui/src/pages/chart/View.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<x-page-header :subtitle="model.title">
|
||||||
|
<template #action>
|
||||||
|
<n-button secondary size="small" @click="$router.push('/system/charts')">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<back-icon />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('buttons.return') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
secondary
|
||||||
|
size="small"
|
||||||
|
@click="$router.push(`/system/charts/${model.id}/edit`)"
|
||||||
|
>{{ t('buttons.edit') }}</n-button>
|
||||||
|
</template>
|
||||||
|
</x-page-header>
|
||||||
|
<n-space class="page-body" vertical :size="16">
|
||||||
|
<x-description :label-width="90">
|
||||||
|
<x-description-item :label="t('fields.id')">{{ model.id }}</x-description-item>
|
||||||
|
<x-description-item :label="t('fields.name')">{{ model.title }}</x-description-item>
|
||||||
|
<x-description-item :span="2" :label="t('fields.desc')">{{ model.desc }}</x-description-item>
|
||||||
|
<x-description-item :label="t('fields.width')">{{ model.width }}</x-description-item>
|
||||||
|
<x-description-item :label="t('fields.height')">{{ model.height }}</x-description-item>
|
||||||
|
<x-description-item :label="t('fields.unit')">{{ model.unit }}</x-description-item>
|
||||||
|
<x-description-item :label="t('fields.margin')">
|
||||||
|
<n-space :size="4" v-if="model.margin">
|
||||||
|
<x-pair-tag
|
||||||
|
type="warning"
|
||||||
|
:label="t('fields.left')"
|
||||||
|
:value="model.margin.left.toString()"
|
||||||
|
v-if="model.margin?.left"
|
||||||
|
/>
|
||||||
|
<x-pair-tag
|
||||||
|
type="warning"
|
||||||
|
:label="t('fields.right')"
|
||||||
|
:value="model.margin.right.toString()"
|
||||||
|
v-if="model.margin?.right"
|
||||||
|
/>
|
||||||
|
<x-pair-tag
|
||||||
|
type="warning"
|
||||||
|
:label="t('fields.top')"
|
||||||
|
:value="model.margin.top.toString()"
|
||||||
|
v-if="model.margin?.top"
|
||||||
|
/>
|
||||||
|
<x-pair-tag
|
||||||
|
type="warning"
|
||||||
|
:label="t('fields.bottom')"
|
||||||
|
:value="model.margin.bottom.toString()"
|
||||||
|
v-if="model.margin?.bottom"
|
||||||
|
/>
|
||||||
|
</n-space>
|
||||||
|
</x-description-item>
|
||||||
|
<x-description-item :label="t('fields.dashboard')">{{ model.dashboard }}</x-description-item>
|
||||||
|
<x-description-item :label="t('fields.type')">{{ model.type }}</x-description-item>
|
||||||
|
<x-description-item :label="t('fields.created_at')">{{ model.createdAt }}</x-description-item>
|
||||||
|
<x-description-item :label="t('fields.updated_at')">{{ model.updatedAt }}</x-description-item>
|
||||||
|
</x-description>
|
||||||
|
<x-panel :title="t('fields.metrics')">
|
||||||
|
<n-table size="small" :bordered="true" :single-line="false">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ t('fields.legend') }}</th>
|
||||||
|
<th>{{ t('fields.query') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="m in model.metrics">
|
||||||
|
<td>{{ m.legend }}</td>
|
||||||
|
<td>{{ m.query }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</n-table>
|
||||||
|
</x-panel>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import {
|
||||||
|
NButton,
|
||||||
|
NSpace,
|
||||||
|
NIcon,
|
||||||
|
NTable,
|
||||||
|
} from "naive-ui";
|
||||||
|
import { ArrowBackCircleOutline as BackIcon } from "@vicons/ionicons5";
|
||||||
|
import XPageHeader from "@/components/PageHeader.vue";
|
||||||
|
import XPairTag from "@/components/PairTag.vue";
|
||||||
|
import XPanel from "@/components/Panel.vue";
|
||||||
|
import chartApi from "@/api/chart";
|
||||||
|
import type { Chart } from "@/api/chart";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { XDescription, XDescriptionItem } from "@/components/description";
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const route = useRoute();
|
||||||
|
const model = ref({} as Chart);
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
const id = route.params.id as string;
|
||||||
|
let r = await chartApi.find(id);
|
||||||
|
model.value = r.data as Chart;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(fetchData);
|
||||||
|
</script>
|
@ -21,7 +21,12 @@
|
|||||||
style="width: 140px"
|
style="width: 140px"
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
<n-input size="small" v-model:value="filter.name" :placeholder="t('fields.object')" clearable />
|
<n-input
|
||||||
|
size="small"
|
||||||
|
v-model:value="filter.name"
|
||||||
|
:placeholder="t('fields.object')"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
<n-button size="small" type="primary" @click="() => fetchData()">{{ t('buttons.search') }}</n-button>
|
<n-button size="small" type="primary" @click="() => fetchData()">{{ t('buttons.search') }}</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
<n-data-table
|
<n-data-table
|
||||||
@ -107,10 +112,6 @@ const types: any = [
|
|||||||
label: 'Service',
|
label: 'Service',
|
||||||
value: 'Service'
|
value: 'Service'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Template',
|
|
||||||
value: 'Template'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Stack',
|
label: 'Stack',
|
||||||
value: 'Stack'
|
value: 'Stack'
|
||||||
@ -192,14 +193,14 @@ const { state, pagination, fetchData, changePageSize } = useDataTable(eventApi.s
|
|||||||
|
|
||||||
function url(e: Event): string {
|
function url(e: Event): string {
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case "Authentication":
|
case "User":
|
||||||
return `/system/users/${e.code}`
|
return `/system/users/${e.code}`
|
||||||
case "Role":
|
case "Role":
|
||||||
return `/system/roles/${e.code}`
|
return `/system/roles/${e.code}`
|
||||||
case "User":
|
case "Chart":
|
||||||
return `/system/users/${e.code}`
|
return `/system/charts/${e.code}`
|
||||||
case "Setting":
|
case "Setting":
|
||||||
return '/system/setting'
|
return '/system/settings'
|
||||||
case "Registry":
|
case "Registry":
|
||||||
return `/swarm/registries/${e.code}`
|
return `/swarm/registries/${e.code}`
|
||||||
case "Node":
|
case "Node":
|
||||||
@ -208,8 +209,6 @@ function url(e: Event): string {
|
|||||||
return `/swarm/networks/${e.code}`
|
return `/swarm/networks/${e.code}`
|
||||||
case "Service":
|
case "Service":
|
||||||
return `/swarm/services/${e.code}`
|
return `/swarm/services/${e.code}`
|
||||||
case "Template":
|
|
||||||
return `/swarm/templates/${e.code}`
|
|
||||||
case "Stack":
|
case "Stack":
|
||||||
return `/swarm/stacks/${e.code}`
|
return `/swarm/stacks/${e.code}`
|
||||||
case "Config":
|
case "Config":
|
||||||
|
@ -281,6 +281,11 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: "/system/charts",
|
path: "/system/charts",
|
||||||
component: () => import('../pages/chart/List.vue'),
|
component: () => import('../pages/chart/List.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "chart_detail",
|
||||||
|
path: "/system/charts/:id",
|
||||||
|
component: () => import('../pages/chart/View.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "chart_new",
|
name: "chart_new",
|
||||||
path: "/system/charts/new",
|
path: "/system/charts/new",
|
||||||
|
Loading…
Reference in New Issue
Block a user