mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 12:57:08 +00:00
Add user management page to frontend
This commit is contained in:
@@ -17,6 +17,7 @@ import QC from '../views/QC.vue'
|
|||||||
import Graphs from '../views/Graphs.vue'
|
import Graphs from '../views/Graphs.vue'
|
||||||
import Map from '../views/Map.vue'
|
import Map from '../views/Map.vue'
|
||||||
import ProjectSettings from '../views/ProjectSettings.vue'
|
import ProjectSettings from '../views/ProjectSettings.vue'
|
||||||
|
import Users from '../views/Users.vue'
|
||||||
import DougalAppBarExtensionProject from '../components/app-bar-extension-project'
|
import DougalAppBarExtensionProject from '../components/app-bar-extension-project'
|
||||||
import DougalAppBarExtensionProjectList from '../components/app-bar-extension-project-list'
|
import DougalAppBarExtensionProjectList from '../components/app-bar-extension-project-list'
|
||||||
|
|
||||||
@@ -49,6 +50,19 @@ Vue.use(VueRouter)
|
|||||||
name: "equipment",
|
name: "equipment",
|
||||||
component: () => import(/* webpackChunkName: "about" */ '../views/Equipment.vue')
|
component: () => import(/* webpackChunkName: "about" */ '../views/Equipment.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
pathToRegexpOptions: { strict: true },
|
||||||
|
path: "/users",
|
||||||
|
redirect: "/users/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pathToRegexpOptions: { strict: true },
|
||||||
|
name: "Users",
|
||||||
|
path: "/users/",
|
||||||
|
component: Users,
|
||||||
|
meta: {
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
pathToRegexpOptions: { strict: true },
|
pathToRegexpOptions: { strict: true },
|
||||||
path: "/login",
|
path: "/login",
|
||||||
|
|||||||
451
lib/www/client/source/src/views/Users.vue
Normal file
451
lib/www/client/source/src/views/Users.vue
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
<template>
|
||||||
|
<v-container fluid>
|
||||||
|
|
||||||
|
<v-overlay :value="loading && !users" absolute>
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
size="64"
|
||||||
|
></v-progress-circular>
|
||||||
|
</v-overlay>
|
||||||
|
|
||||||
|
<v-overlay :value="!users && !loading" absolute opacity="0.8">
|
||||||
|
<v-row justify="center">
|
||||||
|
<v-alert
|
||||||
|
type="error"
|
||||||
|
>
|
||||||
|
The configuration could not be loaded.
|
||||||
|
</v-alert>
|
||||||
|
</v-row>
|
||||||
|
<v-row justify="center">
|
||||||
|
<v-btn color="primary" @click="getUsers">Retry</v-btn>
|
||||||
|
</v-row>
|
||||||
|
</v-overlay>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="4" max-height="100%">
|
||||||
|
<v-toolbar
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
>
|
||||||
|
<v-toolbar-title>
|
||||||
|
User management
|
||||||
|
</v-toolbar-title>
|
||||||
|
<v-btn v-if="!showIP && !showHost"
|
||||||
|
class="ml-3"
|
||||||
|
small
|
||||||
|
icon
|
||||||
|
title="Show password field (allows changing a user's password)"
|
||||||
|
@click="showPassword = !showPassword"
|
||||||
|
>
|
||||||
|
<v-icon small>{{ showPassword ? "mdi-eye" : "mdi-eye-off" }}</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer/>
|
||||||
|
<template v-if="isDirty">
|
||||||
|
<v-icon color="warning" @click="reset" title="Discard changes">mdi-undo</v-icon>
|
||||||
|
<v-spacer/>
|
||||||
|
<v-icon left color="primary" @click="saveToFile" title="Save changes to file">mdi-content-save-outline</v-icon>
|
||||||
|
<v-icon color="primary" @click="upload" title="Upload changes to server">mdi-cloud-upload</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<v-btn
|
||||||
|
class="mb-5"
|
||||||
|
color="primary"
|
||||||
|
small
|
||||||
|
@click="addUser"
|
||||||
|
>
|
||||||
|
<v-icon left small>mdi-account-plus-outline</v-icon>
|
||||||
|
Add user
|
||||||
|
</v-btn>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<v-menu
|
||||||
|
offset-y
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<v-btn
|
||||||
|
class="mb-5"
|
||||||
|
color="primary"
|
||||||
|
small
|
||||||
|
v-bind="attrs"
|
||||||
|
v-on="on"
|
||||||
|
>
|
||||||
|
<v-icon left small>mdi-account-plus-outline</v-icon>
|
||||||
|
Add user
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<v-btn
|
||||||
|
class="mb-5"
|
||||||
|
width="100%"
|
||||||
|
color="blue"
|
||||||
|
dark
|
||||||
|
small
|
||||||
|
@click="addUser({password: true})"
|
||||||
|
>
|
||||||
|
Add named user
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-btn
|
||||||
|
class="mb-5"
|
||||||
|
width="100%"
|
||||||
|
color="green"
|
||||||
|
dark
|
||||||
|
small
|
||||||
|
@click="addUser({ip: true})"
|
||||||
|
>
|
||||||
|
Add IP user
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-btn
|
||||||
|
class="mb-5"
|
||||||
|
width="100%"
|
||||||
|
color="cyan"
|
||||||
|
dark
|
||||||
|
small
|
||||||
|
@click="addUser({host: true})"
|
||||||
|
>
|
||||||
|
Add host user
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
<v-treeview
|
||||||
|
ref="tree"
|
||||||
|
dense
|
||||||
|
open-on-click
|
||||||
|
activatable
|
||||||
|
hoverable
|
||||||
|
:multiple-active="false"
|
||||||
|
:active.sync="active"
|
||||||
|
:open.sync="open"
|
||||||
|
:items="treeview"
|
||||||
|
style="cursor:pointer;"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend="{ item, open }">
|
||||||
|
<v-icon v-if="userIsDirty(item.value)"
|
||||||
|
small
|
||||||
|
color="warning"
|
||||||
|
title="Discard changes"
|
||||||
|
@click="reset(item)"
|
||||||
|
>mdi-undo</v-icon>
|
||||||
|
</template>
|
||||||
|
<template v-slot:label="{ item, open }">
|
||||||
|
<v-chip v-if="item.value"
|
||||||
|
small
|
||||||
|
>
|
||||||
|
{{ item.id }}
|
||||||
|
</v-chip>
|
||||||
|
<template v-else>
|
||||||
|
{{ item.id }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-slot:append="{ item, open }">
|
||||||
|
<v-icon v-if="userIsDirty(item.value)"
|
||||||
|
small
|
||||||
|
color="primary"
|
||||||
|
title="Save changes"
|
||||||
|
@click.stop="saveUser(item.value)"
|
||||||
|
>mdi-cloud-upload</v-icon>
|
||||||
|
<v-btn v-else-if="item.value"
|
||||||
|
small
|
||||||
|
icon
|
||||||
|
@click="remove(item)"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
small
|
||||||
|
color="danger"
|
||||||
|
title="Delete this user"
|
||||||
|
>mdi-trash-can-outline</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-treeview>
|
||||||
|
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="8">
|
||||||
|
<dougal-user-settings v-if="newUser"
|
||||||
|
:self="user"
|
||||||
|
:show-ip="showIP"
|
||||||
|
:show-host="showHost"
|
||||||
|
:show-password="showPassword"
|
||||||
|
v-model="newUser"
|
||||||
|
>
|
||||||
|
<template v-slot:actions="{ hasErrors }">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:disabled="hasErrors"
|
||||||
|
@click="saveUser(newUser)"
|
||||||
|
>Save</v-btn>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="warning"
|
||||||
|
@click="newUser=null"
|
||||||
|
>Cancel</v-btn>
|
||||||
|
</template>
|
||||||
|
</dougal-user-settings>
|
||||||
|
<dougal-user-settings v-else-if="activeUser"
|
||||||
|
:self="user"
|
||||||
|
:show-password="showPassword"
|
||||||
|
v-model="activeUser"
|
||||||
|
>
|
||||||
|
<template v-slot:actions="{ hasErrors, dirty }">
|
||||||
|
<v-btn
|
||||||
|
v-if="dirty"
|
||||||
|
color="primary"
|
||||||
|
:disabled="hasErrors"
|
||||||
|
@click="saveUser(activeUser)"
|
||||||
|
>Save</v-btn>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
v-if="dirty"
|
||||||
|
color="warning"
|
||||||
|
@click="reset(activeUser)"
|
||||||
|
>Cancel</v-btn>
|
||||||
|
</template>
|
||||||
|
</dougal-user-settings>
|
||||||
|
<v-card v-else>
|
||||||
|
<v-card-text>
|
||||||
|
Select an user to edit from the list
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import YAML from 'yaml';
|
||||||
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
|
import { deepSet } from '@/lib/utils';
|
||||||
|
import { User } from '@/lib/user';
|
||||||
|
|
||||||
|
import DougalUserSettings from '@/components/user-settings';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DougalUsers",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
DougalUserSettings
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
users: [],
|
||||||
|
active: [],
|
||||||
|
open: [],
|
||||||
|
newUser: null,
|
||||||
|
showIP: null,
|
||||||
|
showHost: null,
|
||||||
|
showPassword: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
isDirty () {
|
||||||
|
return this.users.some( user => user.dirty );
|
||||||
|
},
|
||||||
|
|
||||||
|
treeview () {
|
||||||
|
const tree = {};
|
||||||
|
|
||||||
|
for (const user of this.users) {
|
||||||
|
if (!tree[user.name]) {
|
||||||
|
tree[user.name] = {
|
||||||
|
id: user.name,
|
||||||
|
name: user.name,
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tree[user.name].children.push({
|
||||||
|
id: user.id,
|
||||||
|
name: user.id,
|
||||||
|
value: user
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(tree);
|
||||||
|
},
|
||||||
|
|
||||||
|
activeID () {
|
||||||
|
return this.active[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
activeUser: {
|
||||||
|
get () {
|
||||||
|
if (this.activeID) {
|
||||||
|
return this.users.find(i => i.id == this.activeID);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set (v) {
|
||||||
|
if (this.activeID) {
|
||||||
|
const idx = this.users.findIndex( i => i.id == this.activeID );
|
||||||
|
if (idx != -1) {
|
||||||
|
this.users.splice(idx, 1, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
async getUsers () {
|
||||||
|
return await User.fromAPI(this.api) ?? [];
|
||||||
|
/*
|
||||||
|
const url = `/user`;
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"If-None-Match": "" // Ensure we get a fresh response
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return await this.api([url, init]);
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
|
||||||
|
userIsDirty (user) {
|
||||||
|
return user?.dirty;
|
||||||
|
},
|
||||||
|
|
||||||
|
addUser (opts={}) {
|
||||||
|
|
||||||
|
this.showIP = opts?.ip;
|
||||||
|
this.showHost = opts?.host;
|
||||||
|
this.showPassword = opts?.password;
|
||||||
|
|
||||||
|
this.newUser = this.user.spawn();
|
||||||
|
// Do not add the wildcard org by default.
|
||||||
|
// It can still be added by hand if needed
|
||||||
|
this.newUser.organisations.remove("*");
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveUser (user) {
|
||||||
|
if (user.api) {
|
||||||
|
console.log("Existing user");
|
||||||
|
} else {
|
||||||
|
console.log("New user");
|
||||||
|
}
|
||||||
|
if (user == this.newUser) {
|
||||||
|
console.log("POST new user");
|
||||||
|
} else {
|
||||||
|
console.log("PUT existing user");
|
||||||
|
}
|
||||||
|
console.log(user);
|
||||||
|
return await user.save(this.api);
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveAll () {
|
||||||
|
let count = 0;
|
||||||
|
const dirtyUsers = this.users.filter( user => user.dirty );
|
||||||
|
for (const user of dirtyUsers) {
|
||||||
|
this.showSnack([`Saving ${user.name} – ${++count} of ${dirtyUsers.length} users`, "info"]);
|
||||||
|
await user.save(this.api);
|
||||||
|
}
|
||||||
|
if (count) {
|
||||||
|
this.showSnack([`${dirtyUsers.length} users saved`, "success"]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async remove (item) {
|
||||||
|
// TODO: Ask for confirmation before removing!
|
||||||
|
const url = `/user/${item.id}`;
|
||||||
|
const init = { method: "DELETE" };
|
||||||
|
const res = await this.api([url, init]);
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
replace(user) {
|
||||||
|
const idx = this.users.findIndex(i => i.id == user.id);
|
||||||
|
if (idx != -1) {
|
||||||
|
this.users.splice(idx, 1, user);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveToFile () {
|
||||||
|
const payload = YAML.stringify(this.users);
|
||||||
|
const blob = new Blob([payload], {type: "application/yaml"});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const filename = "dougal-users.yaml";
|
||||||
|
|
||||||
|
const element = document.createElement('a');
|
||||||
|
element.download = filename;
|
||||||
|
element.href = url;
|
||||||
|
element.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
async upload () {
|
||||||
|
let success = true;
|
||||||
|
const dirty = this.users.filter(i => i.$isDirty);
|
||||||
|
let count = 0;
|
||||||
|
for (const user of dirty) {
|
||||||
|
const body = {...user};
|
||||||
|
delete body.$isDirty;
|
||||||
|
const url = `/user/${user.id}`;
|
||||||
|
const init = {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body
|
||||||
|
};
|
||||||
|
const res = await this.api([url, init]);
|
||||||
|
if (res && res.id) {
|
||||||
|
// In case the server decided to apply any changes
|
||||||
|
this.showSnack([`User ${user.id} uploaded to server (${++count}/${dirty.length})`, "success"]);
|
||||||
|
this.$nextTick( () => {
|
||||||
|
this.replace(res);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
success = false;
|
||||||
|
this.showSnack([`Failed to save user ${user.name} (${user.id})`, "warning"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async reset (item) {
|
||||||
|
this.active = [];
|
||||||
|
this.open = [];
|
||||||
|
const users = await this.getUsers();
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
// Reset only this user
|
||||||
|
const id = item.id;
|
||||||
|
|
||||||
|
const idx0 = this.users.findIndex(i => i.id == id);
|
||||||
|
const idx1 = users.findIndex(i => i.id == id);
|
||||||
|
|
||||||
|
if (idx0 != -1 && idx1 != -1) {
|
||||||
|
this.users.splice(idx0, 1, users[idx1]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reset all
|
||||||
|
this.users = users;
|
||||||
|
}
|
||||||
|
this.$refs.tree.updateAll(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
...mapActions(["api", "showSnack"])
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted () {
|
||||||
|
await this.reset();
|
||||||
|
this.$refs.tree.updateAll(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user