Make event objects more extensible

This commit is contained in:
cuigh 2021-12-24 15:04:15 +08:00
parent 308d3ddc05
commit 752ddff01f
24 changed files with 208 additions and 384 deletions

View File

@ -63,7 +63,7 @@ func (b *containerBiz) Search(node, name, status string, pageIndex, pageSize int
func (b *containerBiz) Delete(node, id, name string, user web.User) (err error) {
err = b.d.ContainerRemove(context.TODO(), node, id)
if err == nil {
b.eb.CreateContainer(EventActionDelete, id, name, user)
b.eb.CreateContainer(EventActionDelete, node, id, name, user)
}
return
}
@ -92,7 +92,7 @@ func (b *containerBiz) Prune(node string, user web.User) (count int, size uint64
var report types.ContainersPruneReport
if report, err = b.d.ContainerPrune(context.TODO(), node); err == nil {
count, size = len(report.ContainersDeleted), report.SpaceReclaimed
b.eb.CreateContainer(EventActionPrune, "", "", user)
b.eb.CreateContainer(EventActionPrune, node, "", "", user)
}
return
}

View File

@ -3,6 +3,7 @@ package biz
import (
"context"
"github.com/cuigh/auxo/data"
"github.com/cuigh/auxo/log"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/dao"
@ -16,7 +17,6 @@ const (
EventTypeNode EventType = "Node"
EventTypeNetwork EventType = "Network"
EventTypeService EventType = "Service"
EventTypeServiceTemplate EventType = "Template"
EventTypeStack EventType = "Stack"
EventTypeConfig EventType = "Config"
EventTypeSecret EventType = "Secret"
@ -51,13 +51,12 @@ type EventBiz interface {
CreateNode(action EventAction, id, name string, user web.User)
CreateNetwork(action EventAction, id, name string, user web.User)
CreateService(action EventAction, name string, user web.User)
CreateTemplate(action EventAction, id, name string, user web.User)
CreateConfig(action EventAction, id, name string, user web.User)
CreateSecret(action EventAction, id, name string, user web.User)
CreateStack(action EventAction, name string, user web.User)
CreateImage(action EventAction, id string, user web.User)
CreateContainer(action EventAction, id, name string, user web.User)
CreateVolume(action EventAction, name string, user web.User)
CreateImage(action EventAction, node, id string, user web.User)
CreateContainer(action EventAction, node, id, name string, user web.User)
CreateVolume(action EventAction, node, name string, user web.User)
CreateUser(action EventAction, id, name string, user web.User)
CreateRole(action EventAction, id, name string, user web.User)
CreateChart(action EventAction, id, title string, user web.User)
@ -76,79 +75,99 @@ func (b *eventBiz) Search(args *dao.EventSearchArgs) (events []*dao.Event, total
return b.d.EventSearch(context.TODO(), args)
}
func (b *eventBiz) create(et EventType, ea EventAction, code, name string, user web.User) {
func (b *eventBiz) create(et EventType, ea EventAction, args data.Map, user web.User) {
event := &dao.Event{
ID: primitive.NewObjectID(),
Type: string(et),
Action: string(ea),
Code: code,
Name: name,
Args: args,
UserID: user.ID(),
Username: user.Name(),
Time: now(),
}
err := b.d.EventCreate(context.TODO(), event)
if err != nil {
log.Get("event").Errorf("failed to create event `%+v`: %v", event, err)
log.Get("event").Errorf("failed to create event `%+v`: %s", event, err)
}
}
func (b *eventBiz) CreateRegistry(action EventAction, id, name string, user web.User) {
b.create(EventTypeRegistry, action, id, name, user)
args := data.Map{"id": id, "name": name}
b.create(EventTypeRegistry, action, args, user)
}
func (b *eventBiz) CreateService(action EventAction, name string, user web.User) {
b.create(EventTypeService, action, name, name, user)
}
func (b *eventBiz) CreateTemplate(action EventAction, id, name string, user web.User) {
b.create(EventTypeServiceTemplate, action, id, name, user)
args := data.Map{"name": name}
b.create(EventTypeService, action, args, user)
}
func (b *eventBiz) CreateNetwork(action EventAction, id, name string, user web.User) {
b.create(EventTypeNetwork, action, id, name, user)
args := data.Map{"id": id, "name": name}
b.create(EventTypeNetwork, action, args, user)
}
func (b *eventBiz) CreateNode(action EventAction, id, name string, user web.User) {
b.create(EventTypeNode, action, id, name, user)
args := data.Map{"id": id, "name": name}
b.create(EventTypeNode, action, args, user)
}
func (b *eventBiz) CreateImage(action EventAction, id string, user web.User) {
b.create(EventTypeImage, action, id, "", user)
func (b *eventBiz) CreateImage(action EventAction, node, id string, user web.User) {
args := data.Map{"node": node}
if id != "" {
args["id"] = id
}
b.create(EventTypeImage, action, args, user)
}
func (b *eventBiz) CreateContainer(action EventAction, id, name string, user web.User) {
b.create(EventTypeContainer, action, id, name, user)
func (b *eventBiz) CreateContainer(action EventAction, node, id, name string, user web.User) {
args := data.Map{"node": node}
if id != "" {
args["id"] = id
}
if name != "" {
args["name"] = name
}
b.create(EventTypeContainer, action, args, user)
}
func (b *eventBiz) CreateVolume(action EventAction, name string, user web.User) {
b.create(EventTypeVolume, action, name, name, user)
func (b *eventBiz) CreateVolume(action EventAction, node, name string, user web.User) {
args := data.Map{"node": node}
if name != "" {
args["name"] = name
}
b.create(EventTypeVolume, action, args, user)
}
func (b *eventBiz) CreateStack(action EventAction, name string, user web.User) {
b.create(EventTypeStack, action, name, name, user)
args := data.Map{"name": name}
b.create(EventTypeStack, action, args, user)
}
func (b *eventBiz) CreateSecret(action EventAction, id, name string, user web.User) {
b.create(EventTypeSecret, action, id, name, user)
args := data.Map{"id": id, "name": name}
b.create(EventTypeSecret, action, args, user)
}
func (b *eventBiz) CreateConfig(action EventAction, id, name string, user web.User) {
b.create(EventTypeConfig, action, id, name, user)
args := data.Map{"id": id, "name": name}
b.create(EventTypeConfig, action, args, user)
}
func (b *eventBiz) CreateRole(action EventAction, id, name string, user web.User) {
b.create(EventTypeRole, action, id, name, user)
args := data.Map{"id": id, "name": name}
b.create(EventTypeRole, action, args, user)
}
func (b *eventBiz) CreateSetting(action EventAction, user web.User) {
b.create(EventTypeSetting, action, "", "Setting", user)
b.create(EventTypeSetting, action, nil, user)
}
func (b *eventBiz) CreateUser(action EventAction, id, name string, user web.User) {
b.create(EventTypeUser, action, id, name, user)
args := data.Map{"id": id, "name": name}
b.create(EventTypeUser, action, args, user)
}
func (b *eventBiz) CreateChart(action EventAction, id, title string, user web.User) {
b.create(EventTypeChart, action, id, title, user)
args := data.Map{"id": id, "name": title}
b.create(EventTypeChart, action, args, user)
}

View File

@ -65,7 +65,7 @@ func (b *imageBiz) Search(node, name string, pageIndex, pageSize int) (images []
func (b *imageBiz) Delete(node, id string, user web.User) (err error) {
err = b.d.ImageRemove(context.TODO(), node, id)
if err == nil {
b.eb.CreateImage(EventActionDelete, id, user)
b.eb.CreateImage(EventActionDelete, node, id, user)
}
return
}
@ -74,7 +74,7 @@ func (b *imageBiz) Prune(node string, user web.User) (count int, size uint64, er
var report types.ImagesPruneReport
if report, err = b.d.ImagePrune(context.TODO(), node); err == nil {
count, size = len(report.ImagesDeleted), report.SpaceReclaimed
b.eb.CreateImage(EventActionPrune, "", user)
b.eb.CreateImage(EventActionPrune, node, "", user)
}
return
}

View File

@ -60,7 +60,7 @@ func (b *volumeBiz) Search(node, name string, pageIndex, pageSize int) (volumes
func (b *volumeBiz) Delete(node, name string, user web.User) (err error) {
err = b.d.VolumeRemove(context.TODO(), node, name)
if err == nil {
b.eb.CreateVolume(EventActionDelete, name, user)
b.eb.CreateVolume(EventActionDelete, node, name, user)
}
return
}
@ -79,8 +79,8 @@ func (b *volumeBiz) Create(vol *Volume, user web.User) (err error) {
}
err = b.d.VolumeCreate(context.TODO(), vol.Node, options)
if err != nil {
b.eb.CreateVolume(EventActionDelete, vol.Name, user)
if err == nil {
b.eb.CreateVolume(EventActionDelete, vol.Node, vol.Name, user)
}
return
}
@ -90,7 +90,7 @@ func (b *volumeBiz) Prune(node string, user web.User) (count int, size uint64, e
report, err = b.d.VolumePrune(context.TODO(), node)
if err == nil {
count, size = len(report.VolumesDeleted), report.SpaceReclaimed
b.eb.CreateVolume(EventActionPrune, "", user)
b.eb.CreateVolume(EventActionPrune, node, "", user)
}
return
}

View File

@ -5,6 +5,7 @@ import (
"sort"
"time"
"github.com/cuigh/auxo/util/cast"
"github.com/cuigh/swirl/dao"
"github.com/cuigh/swirl/misc"
)
@ -21,7 +22,7 @@ func (d *Dao) EventSearch(ctx context.Context, args *dao.EventSearchArgs) (event
match := true
if args.Name != "" {
match = matchAny(args.Name, event.Name)
match = event.Args != nil && matchAny(args.Name, cast.ToString(event.Args["name"]))
}
if match && args.Type != "" {
match = event.Type == args.Type

View File

@ -140,8 +140,7 @@ type Event struct {
ID primitive.ObjectID `json:"id" bson:"_id"`
Type string `json:"type" bson:"type"`
Action string `json:"action" bson:"action"`
Code string `json:"code" bson:"code"`
Name string `json:"name" bson:"name"`
Args data.Map `json:"args" bson:"args"`
UserID string `json:"userId" bson:"user_id"`
Username string `json:"username" bson:"username"`
Time Time `json:"time" bson:"time"`

View File

@ -15,7 +15,7 @@ func (d *Dao) EventSearch(ctx context.Context, args *dao.EventSearchArgs) (event
filter["type"] = args.Type
}
if args.Name != "" {
filter["name"] = args.Name
filter["args.name"] = args.Name
}
opts := searchOptions{filter: filter, sorter: bson.M{"_id": -1}, pageIndex: args.PageIndex, pageSize: args.PageSize}
events = []*dao.Event{}

View File

@ -33,7 +33,7 @@ var indexes = map[string][]mongo.IndexModel{
},
"event": {
mongo.IndexModel{Keys: bson.D{{"type", 1}}},
mongo.IndexModel{Keys: bson.D{{"name", 1}}},
mongo.IndexModel{Keys: bson.D{{"args.name", 1}}},
},
"session": {
mongo.IndexModel{

View File

@ -4,8 +4,9 @@ export interface Event {
id: string;
type: string;
action: string;
code: number;
name: string;
args: {
[key: string]: string;
};
userId: string;
username: string;
time: number;

View File

@ -291,236 +291,3 @@ export function createChart(dom: HTMLElement, info: ChartInfo): Chart {
throw new Error('unknown chart type: ' + info.type)
}
}
// export class ChartDashboardOptions {
// name: string;
// key?: string;
// period?: number = 30;
// refreshInterval?: number = 15; // seconds
// }
// export class ChartDashboard {
// private $panel: JQuery;
// private readonly opts: ChartDashboardOptions;
// private charts: Chart[] = [];
// private timer: number;
// private dlg: ChartDialog;
// constructor(elem: string | Element | JQuery, charts: ChartData[], opts?: ChartDashboardOptions) {
// this.opts = $.extend(new ChartDashboardOptions(), opts);
// this.$panel = $(elem);
// this.dlg = new ChartDialog(this);
// charts.forEach(opts => this.createGraph(opts));
// // events
// Dispatcher.bind(this.$panel).on("remove-chart", e => {
// let name = $(e.target).closest("div.column").data("name");
// Modal.confirm(`Are you sure to delete chart: <strong>${name}</strong>?`, "Delete chart", dlg => {
// this.removeGraph(name);
// dlg.close();
// });
// });
// $(window).resize(e => {
// $.each(this.charts, (i: number, g: Chart) => {
// g.resize();
// });
// });
// this.refresh();
// }
// refresh() {
// if (!this.timer) {
// this.loadData();
// if (this.opts.refreshInterval > 0) {
// this.timer = setTimeout(this.refreshData.bind(this), this.opts.refreshInterval * 1000);
// }
// }
// }
// private refreshData() {
// this.loadData();
// if (this.opts.refreshInterval > 0) {
// this.timer = setTimeout(this.refreshData.bind(this), this.opts.refreshInterval * 1000);
// }
// }
// stop() {
// clearTimeout(this.timer);
// this.timer = 0;
// }
// setPeriod(period: number) {
// this.opts.period = period;
// this.loadData();
// }
// addGraph(opts: ChartData) {
// this.createGraph(opts);
// this.loadData();
// }
// private createGraph(opts: ChartData) {
// for (let i = 0; i < this.charts.length; i++) {
// let chart = this.charts[i];
// if (chart.getOptions().name === opts.name) {
// // chart already added.
// return;
// }
// }
// let chart = ChartFactory.create(opts);
// if (chart != null) {
// this.$panel.append(chart.getElem());
// chart.init();
// this.charts.push(chart);
// }
// }
// removeGraph(name: string) {
// let index = -1;
// for (let i = 0; i < this.charts.length; i++) {
// let c = this.charts[i];
// if (c.getOptions().name === name) {
// index = i;
// break;
// }
// }
// if (index >= 0) {
// let $elem = this.charts[index].getElem();
// this.charts.splice(index, 1);
// $elem.remove();
// }
// }
// save(asDefault: boolean = false) {
// let charts: any = [];
// this.$panel.children().each((index: number, elem: Element) => {
// let name = $(elem).data("name");
// for (let i = 0; i < this.charts.length; i++) {
// let c = this.charts[i];
// if (c.getOptions().name === name) {
// charts.push({
// name: c.getOptions().name,
// width: c.getOptions().width,
// height: c.getOptions().height,
// });
// break;
// }
// }
// });
// let args = {
// name: this.opts.name,
// key: asDefault ? '' : (this.opts.key || ''),
// charts: charts,
// };
// $ajax.post(`/system/chart/save_dashboard`, args).json<AjaxResult>((r: AjaxResult) => {
// if (r.success) {
// Notification.show("success", "Successfully saved.");
// } else {
// Notification.show("danger", r.message);
// }
// });
// }
// getOptions(): ChartDashboardOptions {
// return this.opts;
// }
// private loadData() {
// if (this.charts.length == 0) {
// return
// }
// let args: any = {
// charts: this.charts.map(c => c.getOptions().name).join(","),
// period: this.opts.period,
// };
// if (this.opts.key) {
// args.key = this.opts.key;
// }
// $ajax.get(`/system/chart/data`, args).json((d: { [index: string]: any[] }) => {
// $.each(this.charts, (i: number, g: Chart) => {
// if (d[g.getOptions().name]) {
// g.setData(d[g.getOptions().name]);
// }
// });
// });
// }
// }
// class ChartDialog {
// private dashboard: ChartDashboard;
// private fb: FilterBox;
// private charts: any;
// private $charts: JQuery;
// constructor(dashboard: ChartDashboard) {
// this.dashboard = dashboard;
// this.fb = new FilterBox("#txt-query", this.filterCharts.bind(this));
// $("#btn-add").click(this.showAddDlg.bind(this));
// $("#btn-add-chart").click(this.addChart.bind(this));
// $("#btn-save").click(() => {
// this.dashboard.save();
// });
// $("#btn-save-as-default").click(() => {
// this.dashboard.save(true);
// });
// }
// private showAddDlg() {
// let $panel = $("#nav-charts");
// $panel.find("label.panel-block").remove();
// // load charts
// $ajax.get(`/system/chart/query`, { dashboard: this.dashboard.getOptions().name }).json((charts: any) => {
// for (let i = 0; i < charts.length; i++) {
// let c = charts[i];
// $panel.append(`<label class="panel-block">
// <input type="checkbox" value="${c.name}" data-index="${i}">${c.name}: ${c.title}
// </label>`);
// }
// this.charts = charts;
// this.$charts = $panel.find("label.panel-block");
// });
// let dlg = new Modal("#dlg-add-chart");
// dlg.show();
// }
// private filterCharts(text: string) {
// if (!text) {
// this.$charts.show();
// return;
// }
// this.$charts.each((i, elem) => {
// let $elem = $(elem);
// let texts: string[] = [
// this.charts[i].name.toLowerCase(),
// this.charts[i].title.toLowerCase(),
// this.charts[i].desc.toLowerCase(),
// ];
// for (let i = 0; i < texts.length; i++) {
// let index = texts[i].indexOf(text);
// if (index >= 0) {
// $elem.show();
// return;
// }
// }
// $elem.hide();
// })
// }
// private addChart() {
// this.$charts.each((i, e) => {
// if ($(e).find(":checked").length > 0) {
// let c = this.charts[i];
// this.dashboard.addGraph(c);
// }
// });
// Modal.close();
// }
// }

View File

@ -11,16 +11,15 @@
<script setup lang="ts">
import { useThemeVars } from "naive-ui";
import { RouterLink } from "vue-router";
import { RouterLink, RouteLocationRaw } from "vue-router";
const props = defineProps({
active: Boolean,
title: String,
href: {
type: String,
required: true,
},
});
interface Props {
active?: boolean;
title?: string;
href: RouteLocationRaw;
}
const props = defineProps<Props>()
const themeVars = useThemeVars();
</script>

View File

@ -1,7 +1,7 @@
<template>
<x-page-header>
<template #action>
<n-button secondary size="small" @click="$router.push('/system/charts')">
<n-button secondary size="small" @click="$router.push({ name: 'chart_list' })">
<template #icon>
<n-icon>
<back-icon />
@ -219,7 +219,7 @@ const rules: any = {
const form = ref();
const { submit, submiting } = useForm(form, () => chartApi.save(model.value), () => {
window.message.info(t('texts.action_success'));
router.push("/system/charts")
router.push({ name: 'chart_list' })
})
function newMetric() {

View File

@ -9,18 +9,24 @@
</template>
{{ t('buttons.import') }}
</n-button>
<n-button secondary size="small" @click="$router.push('/system/charts/new')">
<n-button secondary size="small" @click="$router.push({ name: 'chart_new' })">
<template #icon>
<n-icon>
<add-icon />
</n-icon>
</template>{{ t('buttons.new') }}
</template>
{{ t('buttons.new') }}
</n-button>
</template>
</x-page-header>
<n-space class="page-body" vertical :size="12">
<n-space :size="12">
<n-input size="small" v-model:value="filter.title" :placeholder="t('fields.title')" clearable />
<n-input
size="small"
v-model:value="filter.title"
:placeholder="t('fields.title')"
clearable
/>
<n-select
size="small"
:placeholder="t('fields.dashboard')"
@ -84,7 +90,7 @@ const columns = [
title: t('fields.title'),
key: "title",
fixed: "left" as const,
render: (c: Chart) => renderLink(`/system/charts/${c.id}`, c.title),
render: (c: Chart) => renderLink({ name: 'chart_detail', params: { id: c.id } }, c.title),
},
{
title: t('fields.type'),
@ -136,7 +142,7 @@ const columns = [
{
type: 'warning',
text: t('buttons.edit'),
action: () => router.push(`/system/charts/${c.id}/edit`),
action: () => router.push({ name: 'chart_edit', params: { id: c.id } }),
},
{
type: 'info',

View File

@ -1,7 +1,7 @@
<template>
<x-page-header :subtitle="model.title">
<template #action>
<n-button secondary size="small" @click="$router.push('/system/charts')">
<n-button secondary size="small" @click="$router.push({ name: 'chart_list' })">
<template #icon>
<n-icon>
<back-icon />
@ -12,7 +12,7 @@
<n-button
secondary
size="small"
@click="$router.push(`/system/charts/${model.id}/edit`)"
@click="$router.push({ name: 'chart_edit', params: { id: model.id } })"
>{{ t('buttons.edit') }}</n-button>
</template>
</x-page-header>
@ -55,13 +55,19 @@
<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_by')">
<x-anchor :url="`/system/users/${model.createdBy?.id}`">{{ model.createdBy?.name }}</x-anchor>
<x-anchor
:url="{ name: 'user_detail', params: { id: model.createdBy?.id } }"
v-if="model.createdBy?.id"
>{{ model.createdBy?.name }}</x-anchor>
</x-description-item>
<x-description-item :label="t('fields.created_at')">
<n-time :time="model.createdAt" format="y-MM-dd HH:mm:ss" />
</x-description-item>
<x-description-item :label="t('fields.updated_by')">
<x-anchor :url="`/system/users/${model.updatedBy?.id}`">{{ model.updatedBy?.name }}</x-anchor>
<x-anchor
:url="{ name: 'user_detail', params: { id: model.updatedBy?.id } }"
v-if="model.updatedBy?.id"
>{{ model.updatedBy?.name }}</x-anchor>
</x-description-item>
<x-description-item :label="t('fields.updated_at')">
<n-time :time="model.updatedAt" format="y-MM-dd HH:mm:ss" />

View File

@ -153,7 +153,6 @@ const columns = [
key: "id",
width: 210,
fixed: "left" as const,
// render: (e: Event) => renderLink(`/system/events/${e.id}`, e.id),
},
{
title: t('fields.type'),
@ -172,15 +171,12 @@ const columns = [
{
title: t('fields.object'),
key: "name",
render(e: Event) {
const u = url(e)
return u ? renderLink(u, e.name) : e.name
},
render: renderObject,
},
{
title: t('fields.operator'),
key: "name",
render: (e: Event) => renderLink(`/system/users/${e.userId}`, e.username),
render: (e: Event) => e.userId ? renderLink({ name: 'user_detail', params: { id: e.userId } }, e.username) : null,
},
{
title: t('fields.time'),
@ -190,40 +186,40 @@ const columns = [
];
const { state, pagination, fetchData, changePageSize } = useDataTable(eventApi.search, filter)
function url(e: Event): RouteLocationRaw | null {
if (e.type === 'Setting') {
return { name: 'setting' }
} else if (!e.code) {
return null
}
function renderObject(e: Event) {
switch (e.type) {
case "User":
return { name: 'user_detail', params: { id: e.code } }
case "Role":
return { name: 'role_detail', params: { id: e.code } }
case "Chart":
return { name: 'chart_detail', params: { id: e.code } }
case "Registry":
return { name: 'registry_detail', params: { id: e.code } }
case "Node":
return { name: 'node_detail', params: { id: e.code } }
case "Network":
return { name: 'network_detail', params: { name: e.code } }
case "Service":
return { name: 'service_detail', params: { name: e.code } }
case "Stack":
return { name: 'stack_detail', params: { name: e.code } }
case "Config":
return { name: 'config_detail', params: { id: e.code } }
case "Secret":
return { name: 'secret_detail', params: { id: e.code } }
return renderLink({ name: e.type.toLowerCase() + '_detail', params: { id: e.args.id } }, e.args.name)
case "Network":
case "Service":
case "Stack":
return renderLink({ name: e.type.toLowerCase() + '_detail', params: { name: e.args.name } }, e.args.name)
case "Image":
return { name: 'image_detail', params: { node: '-', id: e.code } }
if (e.args.id) {
return renderLink({ name: 'image_detail', params: { node: e.args.node || '-', id: e.args.id } }, e.args.id)
} else {
return renderLink({ name: 'image_list' }, t('objects.image'))
}
case "Container":
return { name: 'container_detail', params: { node: '-', id: e.code } }
if (e.args.id) {
return renderLink({ name: 'container_detail', params: { node: e.args.node || '-', id: e.args.id } }, e.args.name)
} else {
return renderLink({ name: 'container_list' }, t('objects.container'))
}
case "Volume":
return { name: 'volume_detail', params: { node: '-', name: e.code } }
if (e.args.name) {
return renderLink({ name: 'volume_detail', params: { node: e.args.node || '-', name: e.args.name } }, e.args.name)
} else {
return renderLink({ name: 'volume_list' }, t('objects.volume'))
}
case "Setting":
return renderLink({ name: 'setting' }, t('objects.setting'))
}
return null
}

View File

@ -23,13 +23,19 @@
<x-description-item :label="t('fields.url')">{{ model.url }}</x-description-item>
<x-description-item :label="t('fields.login_name')">{{ model.username }}</x-description-item>
<x-description-item :label="t('fields.created_by')">
<x-anchor :url="`/system/users/${model.createdBy?.id}`">{{ model.createdBy?.name }}</x-anchor>
<x-anchor
:url="{ name: 'user_detail', params: { id: model.createdBy?.id } }"
v-if="model.createdBy?.id"
>{{ model.createdBy?.name }}</x-anchor>
</x-description-item>
<x-description-item :label="t('fields.created_at')">
<n-time :time="model.createdAt" format="y-MM-dd HH:mm:ss" />
</x-description-item>
<x-description-item :label="t('fields.updated_by')">
<x-anchor :url="`/system/users/${model.updatedBy?.id}`">{{ model.updatedBy?.name }}</x-anchor>
<x-anchor
:url="{ name: 'user_detail', params: { id: model.updatedBy?.id } }"
v-if="model.updatedBy?.id"
>{{ model.updatedBy?.name }}</x-anchor>
</x-description-item>
<x-description-item :label="t('fields.updated_at')">
<n-time :time="model.updatedAt" format="y-MM-dd HH:mm:ss" />

View File

@ -111,7 +111,7 @@ const rules: any = {
const form = ref();
const { submit, submiting } = useForm(form, () => roleApi.save(model.value), () => {
window.message.info(t('texts.action_success'));
router.push("/system/roles")
router.push({ name: 'role_list' })
})
function checkGroup(key: string, checked: boolean = true) {

View File

@ -29,7 +29,7 @@
<tbody>
<tr v-for="(r, index) of model.roles" :key="r.id">
<td>
<x-anchor :url="`/system/roles/${r.id}`">{{ r.id }}</x-anchor>
<x-anchor :url="{ name: 'role_detail', params: { id: r.id } }">{{ r.id }}</x-anchor>
</td>
<td>{{ r.name }}</td>
<td>{{ r.desc }}</td>

View File

@ -22,13 +22,19 @@
<x-description-item :label="t('fields.name')">{{ model.name }}</x-description-item>
<x-description-item :span="2" :label="t('fields.desc')">{{ model.desc }}</x-description-item>
<x-description-item :label="t('fields.created_by')">
<x-anchor :url="`/system/users/${model.createdBy?.id}`">{{ model.createdBy?.name }}</x-anchor>
<x-anchor
:url="{ name: 'user_detail', params: { id: model.createdBy?.id } }"
v-if="model.createdBy?.id"
>{{ model.createdBy?.name }}</x-anchor>
</x-description-item>
<x-description-item :label="t('fields.created_at')">
<n-time :time="model.createdAt" format="y-MM-dd HH:mm:ss" />
</x-description-item>
<x-description-item :label="t('fields.updated_by')">
<x-anchor :url="`/system/users/${model.updatedBy?.id}`">{{ model.updatedBy?.name }}</x-anchor>
<x-anchor
:url="{ name: 'user_detail', params: { id: model.updatedBy?.id } }"
v-if="model.updatedBy?.id"
>{{ model.updatedBy?.name }}</x-anchor>
</x-description-item>
<x-description-item :label="t('fields.updated_at')">
<n-time :time="model.updatedAt" format="y-MM-dd HH:mm:ss" />

View File

@ -70,7 +70,7 @@
<n-dynamic-input v-model:value="model.labels" #="{ index, value }" :on-create="newLabel">
<n-input :placeholder="t('fields.name')" v-model:value="value.name" />
<div style="height: 34px; line-height: 34px; margin: 0 8px">=</div>
<n-input :placeholder="t('fields.name')" v-model:value="value.value" />
<n-input :placeholder="t('fields.value')" v-model:value="value.value" />
</n-dynamic-input>
</n-form-item-gi>
</n-grid>

View File

@ -21,13 +21,19 @@
<x-description :label-width="90">
<x-description-item :label="t('fields.name')" :span="2">{{ model.name }}</x-description-item>
<x-description-item :label="t('fields.created_by')">
<x-anchor :url="`/system/users/${model.createdBy?.id}`">{{ model.createdBy?.name }}</x-anchor>
<x-anchor
:url="{ name: 'user_detail', params: { id: model.createdBy?.id } }"
v-if="model.createdBy?.id"
>{{ model.createdBy?.name }}</x-anchor>
</x-description-item>
<x-description-item :label="t('fields.created_at')">
<n-time :time="model.createdAt" format="y-MM-dd HH:mm:ss" />
</x-description-item>
<x-description-item :label="t('fields.updated_by')">
<x-anchor :url="`/system/users/${model.updatedBy?.id}`">{{ model.updatedBy?.name }}</x-anchor>
<x-anchor
:url="{ name: 'user_detail', params: { id: model.updatedBy?.id } }"
v-if="model.updatedBy?.id"
>{{ model.updatedBy?.name }}</x-anchor>
</x-description-item>
<x-description-item :label="t('fields.updated_at')">
<n-time :time="model.updatedAt" format="y-MM-dd HH:mm:ss" />

View File

@ -1,7 +1,7 @@
<template>
<x-page-header :subtitle="user.id">
<template #action>
<n-button secondary size="small" @click="$router.push('/system/users')">
<n-button secondary size="small" @click="$router.push({ name: 'user_list' })">
<template #icon>
<n-icon>
<back-icon />
@ -21,10 +21,22 @@
<n-input :placeholder="t('fields.login_name')" v-model:value="user.loginName" />
</n-form-item-gi>
<n-form-item-gi :label="t('fields.password')" path="password" v-if="!user.id">
<n-input type="password" :placeholder="t('fields.password')" v-model:value="user.password" />
<n-input
type="password"
:placeholder="t('fields.password')"
v-model:value="user.password"
/>
</n-form-item-gi>
<n-form-item-gi :label="t('fields.password_confirm')" path="passwordConfirm" v-if="!user.id">
<n-input type="password" :placeholder="t('fields.password_confirm')" v-model:value="user.passwordConfirm" />
<n-form-item-gi
:label="t('fields.password_confirm')"
path="passwordConfirm"
v-if="!user.id"
>
<n-input
type="password"
:placeholder="t('fields.password_confirm')"
v-model:value="user.passwordConfirm"
/>
</n-form-item-gi>
<n-form-item-gi :label="t('fields.email')" path="email">
<n-input :placeholder="t('fields.email')" v-model:value="user.email" />
@ -47,7 +59,12 @@
<n-radio key="ldap" value="ldap">LDAP</n-radio>
</n-radio-group>
</n-form-item-gi>
<n-form-item-gi :label="t('objects.role', 2)" span="2" path="roles" v-if="roles && roles.length">
<n-form-item-gi
:label="t('objects.role', 2)"
span="2"
path="roles"
v-if="roles && roles.length"
>
<n-checkbox-group v-model:value="user.roles">
<n-space item-style="display: flex;">
<n-checkbox :value="r.id" :label="r.name" v-for="r of roles" />
@ -119,7 +136,7 @@ const rules: any = {
const form = ref();
const { submit, submiting } = useForm(form, () => userApi.save(user.value), () => {
window.message.info(t('texts.action_success'));
router.push("/system/users")
router.push({ name: 'user_list' })
})
async function fetchData() {

View File

@ -1,7 +1,7 @@
<template>
<x-page-header>
<template #action>
<n-button secondary size="small" @click="$router.push('/system/users/new')">
<n-button secondary size="small" @click="$router.push({ name: 'user_new' })">
<template #icon>
<n-icon>
<add-icon />
@ -13,19 +13,12 @@
</x-page-header>
<n-space class="page-body" vertical :size="12">
<x-tab>
<x-tab-pane href="/system/users" :active="!$route.query.filter">{{ t('fields.all') }}</x-tab-pane>
<x-tab-pane :href="{ name: 'user_list' }" :active="!$route.query.filter">{{ t('fields.all') }}</x-tab-pane>
<x-tab-pane
href="/system/users?filter=admins"
:active="$route.query.filter === 'admins'"
>{{ t('fields.admins') }}</x-tab-pane>
<x-tab-pane
href="/system/users?filter=active"
:active="$route.query.filter === 'active'"
>{{ t('fields.active') }}</x-tab-pane>
<x-tab-pane
href="/system/users?filter=blocked"
:active="$route.query.filter === 'blocked'"
>{{ t('fields.blocked') }}</x-tab-pane>
:href="{ name: 'user_list', query: { filter: tab } }"
:active="$route.query.filter === tab"
v-for="tab in ['admins', 'active', 'blocked']"
>{{ t('fields.' + tab) }}</x-tab-pane>
</x-tab>
<n-space :size="12">
<n-input size="small" v-model:value="args.name" :placeholder="t('fields.name')" clearable />
@ -84,7 +77,7 @@ const columns = [
{
title: t('fields.id'),
key: "id",
render: (row: User) => renderLink(`/system/users/${row.id}`, row.id),
render: (row: User) => renderLink({ name: 'user_detail', params: { id: row.id } }, row.id),
},
{
title: t('fields.name'),
@ -124,7 +117,7 @@ const columns = [
row.status ?
{ type: 'warning', text: t('buttons.block'), action: () => setStatus(row, 0), prompt: t('prompts.block'), } :
{ type: 'success', text: t('buttons.enable'), action: () => setStatus(row, 1) },
{ type: 'warning', text: t('buttons.edit'), action: () => router.push(`/system/users/${row.id}/edit`) },
{ type: 'warning', text: t('buttons.edit'), action: () => router.push({ name: 'user_edit', params: { id: row.id } }) },
{ type: 'error', text: t('buttons.delete'), action: () => remove(row, index), prompt: t('prompts.delete') },
])
},

View File

@ -1,7 +1,7 @@
<template>
<x-page-header :subtitle="model.user.name">
<template #action>
<n-button secondary size="small" @click="$router.push('/system/users')">
<n-button secondary size="small" @click="$router.push({ name: 'user_list' })">
<template #icon>
<n-icon>
<back-icon />
@ -12,7 +12,7 @@
<n-button
secondary
size="small"
@click="$router.push(`/system/users/${model.user.id}/edit`)"
@click="$router.push({ name: 'user_edit', params: { id: model.user.id } })"
>{{ t('buttons.edit') }}</n-button>
</template>
</x-page-header>
@ -45,7 +45,8 @@
</x-description-item>
<x-description-item :label="t('fields.created_by')">
<x-anchor
:url="`/system/users/${model.user.createdBy?.id}`"
:url="{ name: 'user_detail', params: { id: model.user.createdBy?.id } }"
v-if="model.user.createdBy?.id"
>{{ model.user.createdBy?.name }}</x-anchor>
</x-description-item>
<x-description-item :label="t('fields.created_at')">
@ -53,7 +54,8 @@
</x-description-item>
<x-description-item :label="t('fields.updated_by')">
<x-anchor
:url="`/system/users/${model.user.updatedBy?.id}`"
:url="{ name: 'user_detail', params: { id: model.user.updatedBy?.id } }"
v-if="model.user.updatedBy?.id"
>{{ model.user.updatedBy?.name }}</x-anchor>
</x-description-item>
<x-description-item :label="t('fields.updated_at')">