mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 12:27:07 +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 Map from '../views/Map.vue'
|
||||
import ProjectSettings from '../views/ProjectSettings.vue'
|
||||
import Users from '../views/Users.vue'
|
||||
import DougalAppBarExtensionProject from '../components/app-bar-extension-project'
|
||||
import DougalAppBarExtensionProjectList from '../components/app-bar-extension-project-list'
|
||||
|
||||
@@ -49,6 +50,19 @@ Vue.use(VueRouter)
|
||||
name: "equipment",
|
||||
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 },
|
||||
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