mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 12:27:07 +00:00
Merge branch '177-refactor-users-code' into 'devel'
Refactor users code Closes #177 and #176 See merge request wgp/dougal/software!57
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
max-width="600"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-if="adminaccess"
|
||||
<v-btn v-if="adminaccess()"
|
||||
title="Create a new project from scratch. Generally, it's preferable to clone an existing project (right-click → ‘Clone’)"
|
||||
small
|
||||
outlined
|
||||
@@ -31,6 +31,7 @@
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import DougalProjectSettingsNameIdGeodetics from '@/components/project-settings/name-id-geodetics'
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: 'DougalAppBarExtensionProjectList',
|
||||
@@ -39,6 +40,10 @@ export default {
|
||||
DougalProjectSettingsNameIdGeodetics
|
||||
},
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
dialogOpen: false,
|
||||
@@ -50,10 +55,6 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(["adminaccess"])
|
||||
},
|
||||
|
||||
methods: {
|
||||
async save (data) {
|
||||
this.dialogOpen = false;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-tabs :value="tab" show-arrows v-if="page != 'configuration'">
|
||||
<v-tab v-for="tab, index in tabs" :key="index" link :to="tabLink(tab.href)" v-text="tab.text"></v-tab>
|
||||
<template v-if="adminaccess">
|
||||
<template v-if="adminaccess()">
|
||||
<v-spacer></v-spacer>
|
||||
<v-tab :to="tabLink('configuration')" class="orange--text darken-3" title="Edit project settings"><v-icon small left color="orange darken-3">mdi-cog-outline</v-icon> Settings</v-tab>
|
||||
</template>
|
||||
@@ -15,9 +15,15 @@
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: 'DougalAppBarExtensionProject',
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
tabs: [
|
||||
@@ -44,7 +50,6 @@ export default {
|
||||
return this.tabs.findIndex(t => t.href == this.page);
|
||||
},
|
||||
|
||||
...mapGetters(["adminaccess"])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -127,7 +127,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['user', 'writeaccess', 'loading', 'serverEvent'])
|
||||
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -32,16 +32,61 @@
|
||||
</template>
|
||||
|
||||
<v-list dense>
|
||||
<v-list-item :href="`/settings/equipment`">
|
||||
<v-list-item-title>Equipment list</v-list-item-title>
|
||||
<v-list-item href="/settings/equipment">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Equipment list</v-list-item-title>
|
||||
<v-list-item-subtitle>Manage the list of equipment reported in logs</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action><v-icon small>mdi-view-list</v-icon></v-list-item-action>
|
||||
</v-list-item>
|
||||
<template v-if="false">
|
||||
<v-divider></v-divider>
|
||||
<v-list-item href="/settings">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Local settings</v-list-item-title>
|
||||
<v-list-item-subtitle>Manage this vessel's configuration</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action><v-icon small>mdi-ferry</v-icon></v-list-item-action>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
|
||||
</v-menu>
|
||||
|
||||
|
||||
<v-breadcrumbs :items="path"></v-breadcrumbs>
|
||||
<v-breadcrumbs :items="path">
|
||||
<template v-slot:item="{ item }">
|
||||
<v-breadcrumbs-item :href="item.href" :disabled="item.disabled" v-if="item.organisations">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<span v-bind="attrs" v-on="on">{{ item.text }}</span>
|
||||
</template>
|
||||
<div class="text-overline">Project permissions</div>
|
||||
<v-simple-table dense>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Organisation</th><th>Read</th><th>Write</th><th>Edit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(operations, name) in item.organisations">
|
||||
<td v-if="name == '*'"><v-chip small label color="primary">All</v-chip></td>
|
||||
<td v-else><v-chip small label outlined>{{ name }}</v-chip></td>
|
||||
<td>{{ operations.read ? "✔" : " " }}</td>
|
||||
<td>{{ operations.write ? "✔" : " " }}</td>
|
||||
<td>{{ operations.edit ? "✔" : " " }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-tooltip>
|
||||
</v-breadcrumbs-item>
|
||||
<v-breadcrumbs-item :href="item.href" :disabled="item.disabled" v-else>
|
||||
{{ item.text }}
|
||||
</v-breadcrumbs-item>
|
||||
</template>
|
||||
</v-breadcrumbs>
|
||||
|
||||
<template v-if="$route.name != 'Login'">
|
||||
<v-btn text link to="/login" v-if="!user && !loading">Log in</v-btn>
|
||||
@@ -50,10 +95,37 @@
|
||||
<v-menu
|
||||
offset-y
|
||||
>
|
||||
<template v-slot:activator="{on, attrs}">
|
||||
<v-avatar :color="user.colour || 'primary'" :title="`${user.name} (${user.role})`" v-bind="attrs" v-on="on">
|
||||
<span class="white--text">{{user.name.slice(0, 5)}}</span>
|
||||
</v-avatar>
|
||||
<template v-slot:activator="{ on: menu, attrs }">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on: tooltip }">
|
||||
<v-avatar :color="user.colour || 'primary'" v-bind="attrs" v-on="{...tooltip, ...menu}">
|
||||
<span class="white--text">{{user.name.slice(0, 5)}}</span>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<div class="text-overline">{{ user.name }}</div>
|
||||
<v-card flat class="my-1" v-if="user.description">
|
||||
<v-card-text class="pb-1" v-html="$root.markdown(user.description)">
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-simple-table dense>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Organisation</th><th>Read</th><th>Write</th><th>Edit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="org in user.organisations">
|
||||
<td v-if="org.name == '*'"><v-chip small label color="primary">All</v-chip></td>
|
||||
<td v-else><v-chip small label outlined>{{ org.name }}</v-chip></td>
|
||||
<td>{{ org.operations.read ? "✔" : " " }}</td>
|
||||
<td>{{ org.operations.write ? "✔" : " " }}</td>
|
||||
<td>{{ org.operations.edit ? "✔" : " " }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<v-list dense>
|
||||
@@ -66,8 +138,29 @@
|
||||
</v-list-item>
|
||||
<v-list-item link to="/logout" v-else>
|
||||
<v-list-item-icon><v-icon small>mdi-logout</v-icon></v-list-item-icon>
|
||||
<v-list-item-title>Log out</v-list-item-title>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Log out</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<template v-if="canManageUsers">
|
||||
<v-list-item link to="/users">
|
||||
<v-list-item-icon><v-icon small>mdi-account-multiple</v-icon></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Manage users</v-list-item-title>
|
||||
<v-list-item-subtitle>Add, edit and remove users</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template v-else-if="user && !user.autologin">
|
||||
<v-list-item link :to="`/users/${user.id}`">
|
||||
<v-list-item-icon><v-icon small>mdi-account</v-icon></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>User profile</v-list-item-title>
|
||||
<v-list-item-subtitle>Edit your user profile</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
|
||||
</v-menu>
|
||||
@@ -102,6 +195,19 @@ export default {
|
||||
.pop()?.component;
|
||||
},
|
||||
|
||||
title () {
|
||||
return this.user.name + "\n" + [...this.user.organisations].map( ({name, operations}) => {
|
||||
if (name == "*") name = "All organisations";
|
||||
let str = name+": ";
|
||||
str += [ "read", "write", "edit" ].map( op => operations[op] ? op : null ).filter( op => op ).join(", ");
|
||||
return str;
|
||||
}).join("\n")
|
||||
},
|
||||
|
||||
canManageUsers () {
|
||||
return this.user.organisations.accessToOperation("edit").length;
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'loading'])
|
||||
},
|
||||
|
||||
|
||||
112
lib/www/client/source/src/components/organisations-item.vue
Normal file
112
lib/www/client/source/src/components/organisations-item.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<v-row dense no-gutters>
|
||||
|
||||
<v-col>
|
||||
<slot name="prepend"></slot>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-text-field
|
||||
class="mr-5"
|
||||
dense
|
||||
label="Name"
|
||||
:value="name"
|
||||
:readonly="true"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-checkbox
|
||||
class="mr-3"
|
||||
label="Read"
|
||||
v-model="operations.read"
|
||||
:readonly="readonly"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-checkbox
|
||||
class="mr-3"
|
||||
label="Write"
|
||||
v-model="operations.write"
|
||||
:readonly="readonly"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-checkbox
|
||||
class="mr-3"
|
||||
label="Edit"
|
||||
v-model="operations.edit"
|
||||
:readonly="readonly"
|
||||
></v-checkbox>
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<!-- Just to fill the twelve-column grid -->
|
||||
<!--
|
||||
NOTE: this column could also be used for
|
||||
a popdown menu with additional operations
|
||||
if needed.
|
||||
-->
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<slot name="append"></slot>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { Organisations } from '@dougal/organisations';
|
||||
|
||||
export default {
|
||||
name: "DougalOrganisationsItem",
|
||||
|
||||
props: {
|
||||
name: String,
|
||||
value: Object,
|
||||
readonly: Boolean,
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
operations: {...this.value}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value: {
|
||||
handler (newValue) {
|
||||
this.operations = {...this.value};
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
|
||||
operations: {
|
||||
handler (newValue) {
|
||||
if (["read", "write", "edit"].some( k => newValue[k] != this.value[k] )) {
|
||||
// Only emit if a value has actually changed
|
||||
this.$emit("input", {...newValue});
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
reset () {
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
191
lib/www/client/source/src/components/organisations.vue
Normal file
191
lib/www/client/source/src/components/organisations.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Organisations</v-card-title>
|
||||
<v-card-subtitle>Organisation access</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
|
||||
<v-container>
|
||||
|
||||
<dougal-organisations-item v-for="organisation in localOrganisations.names()"
|
||||
:key="organisation"
|
||||
:name="organisation"
|
||||
:value="localOrganisations.get(organisation)"
|
||||
@input="setOrganisation(organisation, $event)"
|
||||
>
|
||||
<template v-slot:append v-if="!readonly">
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
fab
|
||||
text
|
||||
small
|
||||
title="Remove this organisation"
|
||||
>
|
||||
<v-icon
|
||||
color="error"
|
||||
@click="removeOrganisation(organisation)"
|
||||
>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</dougal-organisations-item>
|
||||
|
||||
|
||||
<v-row no-gutters class="mb-2" v-if="!readonly">
|
||||
<h4>Add organisation</h4>
|
||||
</v-row>
|
||||
|
||||
<v-row no-gutters class="mb-2" v-if="!readonly">
|
||||
<v-combobox v-if="canCreateOrganisations"
|
||||
label="Organisation"
|
||||
:items="remainingOrganisations"
|
||||
v-model="organisationName"
|
||||
@input.native="organisationName = $event.srcElement.value"
|
||||
@keyup.enter="addOrganisation()"
|
||||
></v-combobox>
|
||||
<v-select v-else
|
||||
label="Organisation"
|
||||
:items="remainingOrganisations"
|
||||
v-model="organisationName"
|
||||
></v-select>
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
fab
|
||||
text
|
||||
small
|
||||
title="Add organisation"
|
||||
:disabled="!(organisationName && organisationName.length)"
|
||||
@click="addOrganisation()"
|
||||
>
|
||||
<v-icon
|
||||
color="primary"
|
||||
>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<slot name="actions" v-bind="{ self, organisations, readonly, validationErrors, canCreateOrganisations }">
|
||||
</slot>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Organisations } from '@dougal/organisations';
|
||||
import DougalOrganisationsItem from './organisations-item';
|
||||
|
||||
|
||||
export default {
|
||||
name: "DougalOrganisations",
|
||||
|
||||
components: {
|
||||
DougalOrganisationsItem
|
||||
},
|
||||
|
||||
props: {
|
||||
self: Object,
|
||||
organisations: Object,
|
||||
readonly: Boolean
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
organisationName: "",
|
||||
localOrganisations: this.setLocalOrganisations(this.organisations)
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
availableOrganisations () {
|
||||
return this.self.organisations.names();
|
||||
},
|
||||
|
||||
// Organisations available to add.
|
||||
// These are the organisations in `availableOrganisations`
|
||||
// minus any that have already been added.
|
||||
// The special value "*" (meaning "every organisation")
|
||||
// is not included.
|
||||
remainingOrganisations () {
|
||||
const orgs = [];
|
||||
|
||||
for (const org of this.availableOrganisations) {
|
||||
if (org != "*" && !this.localOrganisations.has(org)) {
|
||||
orgs.push(org);
|
||||
}
|
||||
}
|
||||
|
||||
return orgs;
|
||||
},
|
||||
|
||||
canCreateOrganisations () {
|
||||
return this.self.organisations.value("*")?.edit ?? false;
|
||||
},
|
||||
|
||||
validationErrors () {
|
||||
const errors = [];
|
||||
|
||||
// Check if there is at least one organisation
|
||||
if (this.localOrganisations.length) {
|
||||
errors.push("ERR_NO_ORGS");
|
||||
}
|
||||
|
||||
// Check if at least one organisation has edit rights
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
organisations (newValue) {
|
||||
this.localOrganisations = this.setLocalOrganisations(newValue);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
setLocalOrganisations (value) {
|
||||
return new Organisations(this.organisations);
|
||||
},
|
||||
|
||||
setOrganisation(name, value) {
|
||||
this.localOrganisations.set(name, value);
|
||||
this.$emit("update:organisations", new Organisations(this.localOrganisations));
|
||||
},
|
||||
|
||||
addOrganisation () {
|
||||
const key = this.organisationName;
|
||||
if (!this.localOrganisations.has(key)) {
|
||||
this.localOrganisations.set(key);
|
||||
this.$emit("update:organisations", this.localOrganisations);
|
||||
}
|
||||
this.organisationName = "";
|
||||
},
|
||||
|
||||
removeOrganisation (key) {
|
||||
if (this.localOrganisations.has(key)) {
|
||||
this.localOrganisations.remove(key);
|
||||
}
|
||||
this.$emit("update:organisations", this.localOrganisations);
|
||||
},
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<dougal-organisations
|
||||
:self="user"
|
||||
:organisations.sync="organisations_"
|
||||
>
|
||||
<template v-slot:actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
@click="back"
|
||||
>Back</v-btn>
|
||||
</template>
|
||||
</dougal-organisations>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import DougalOrganisations from '../organisations'
|
||||
|
||||
|
||||
export default {
|
||||
name: "DougalProjectSettingsOrganisations",
|
||||
|
||||
components: {
|
||||
DougalOrganisations
|
||||
},
|
||||
|
||||
props: {
|
||||
organisations: Object,
|
||||
value: Object
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
organisations_: {
|
||||
get () {
|
||||
return this.organisations;
|
||||
},
|
||||
|
||||
set (v) {
|
||||
this.$emit("input", {
|
||||
...this.value,
|
||||
organisations: v.toJSON()
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
256
lib/www/client/source/src/components/user-settings.vue
Normal file
256
lib/www/client/source/src/components/user-settings.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
User {{ name }} <v-chip class="mx-3" small>{{id}}</v-chip>
|
||||
<v-chip v-if="self.id == value.id"
|
||||
small
|
||||
color="primary"
|
||||
>It's me!</v-chip>
|
||||
</v-card-title>
|
||||
<v-card-subtitle>User settings</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<!--
|
||||
<v-text-field
|
||||
label="User ID"
|
||||
hint="Unique user ID (read-only)"
|
||||
persistent-hint
|
||||
readonly
|
||||
disabled
|
||||
v-model="id"
|
||||
>
|
||||
</v-text-field>
|
||||
-->
|
||||
|
||||
<v-switch
|
||||
dense
|
||||
label="Active"
|
||||
:title="(self.id == value.id) ? 'You cannot make yourself inactive' : active ? 'Make this user inactive' : 'Make this user active'"
|
||||
:disabled="self.id == value.id"
|
||||
v-model="active"
|
||||
></v-switch>
|
||||
|
||||
<label class="mr-3 pt-5">Colour
|
||||
<v-menu v-model="colourMenu"
|
||||
:close-on-content-click="false"
|
||||
offset-y
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn
|
||||
:title="colour"
|
||||
dense
|
||||
small
|
||||
icon
|
||||
v-on="on"
|
||||
><v-icon :color="colour">mdi-palette</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-color-picker
|
||||
dot-size="25"
|
||||
mode="hexa"
|
||||
swatches-max-height="200"
|
||||
v-model="colour"
|
||||
></v-color-picker>
|
||||
</v-menu>
|
||||
</label>
|
||||
|
||||
<v-text-field
|
||||
v-if="showIp || ip"
|
||||
label="IP address"
|
||||
hint="IP address or subnet specification for auto-login"
|
||||
v-model="ip"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-if="showHost || host"
|
||||
label="Host name"
|
||||
hint="Hostname (for auto-login)"
|
||||
v-model="host"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-text-field
|
||||
label="Name"
|
||||
hint="User name"
|
||||
v-model="name"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-if="showPasswordField"
|
||||
:type="visiblePassword ? 'text' : 'password'"
|
||||
:append-icon="visiblePassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="visiblePassword = !visiblePassword"
|
||||
label="Password"
|
||||
hint="User password"
|
||||
v-model="password"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-text-field
|
||||
label="Email"
|
||||
hint="Email address"
|
||||
v-model="email"
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
<v-textarea
|
||||
class="mb-5"
|
||||
label="Remarks"
|
||||
hint="User description (visible to the user)"
|
||||
auto-grow
|
||||
v-model="description"
|
||||
></v-textarea>
|
||||
|
||||
<dougal-organisations
|
||||
:self="self"
|
||||
:organisations.sync="organisations"
|
||||
></dougal-organisations>
|
||||
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<slot name="actions" v-bind="{ isValid, hasErrors, errors, dirty }"></slot>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { User } from '@/lib/user';
|
||||
|
||||
import DougalOrganisations from './organisations'
|
||||
|
||||
export default {
|
||||
name: "DougalUserSettings",
|
||||
|
||||
components: {
|
||||
DougalOrganisations
|
||||
},
|
||||
|
||||
props: {
|
||||
value: Object,
|
||||
self: Object, // User calling the dialogue
|
||||
|
||||
// The next three props determine whether the
|
||||
// ip, host, and password fields are shown even
|
||||
// when null / empty. If non-null, those fields
|
||||
// are always shown
|
||||
showIp: { type: Boolean, default: false },
|
||||
showHost: { type: Boolean, default: false },
|
||||
showPassword: { type: Boolean, default: false },
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
colourMenu: null,
|
||||
visiblePassword: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
id () { return this.value.id },
|
||||
|
||||
ip: {
|
||||
get () { return this.value.ip },
|
||||
set (v) { this.input("ip", v) }
|
||||
},
|
||||
|
||||
host: {
|
||||
get () { return this.value.host },
|
||||
set (v) { this.input("host", v) }
|
||||
},
|
||||
|
||||
name: {
|
||||
get () { return this.value.name },
|
||||
set (v) { this.input("name", v) }
|
||||
},
|
||||
|
||||
password: {
|
||||
get () { return this.value.password },
|
||||
set (v) { this.input("password", v) }
|
||||
},
|
||||
|
||||
active: {
|
||||
get () { return this.value.active },
|
||||
set (v) { this.input("active", v) }
|
||||
},
|
||||
|
||||
email: {
|
||||
get () { return this.value.email },
|
||||
set (v) { this.input("email", v) }
|
||||
},
|
||||
|
||||
colour: {
|
||||
get () { return this.value.colour },
|
||||
set (v) { this.input("colour", v) }
|
||||
},
|
||||
|
||||
description: {
|
||||
get () { return this.value.description },
|
||||
set (v) { this.input("description", v) }
|
||||
},
|
||||
|
||||
organisations: {
|
||||
get () { return this.value.organisations },
|
||||
set (v) { this.input("organisations", v) }
|
||||
},
|
||||
|
||||
errors () {
|
||||
return this.value.errors;
|
||||
},
|
||||
|
||||
hasErrors () {
|
||||
return !this.isValid;
|
||||
},
|
||||
|
||||
isValid () {
|
||||
return this.value.isValid;
|
||||
},
|
||||
|
||||
dirty () {
|
||||
return this.value?.dirty ?? false;
|
||||
},
|
||||
|
||||
showPasswordField () {
|
||||
return this.password || (this.showPassword &&
|
||||
!(this.showIp || this.ip || this.showHost || this.host));
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
validationErrors () {
|
||||
this.$emit("update:errors", this.validationErrors);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
input (k, v) {
|
||||
const user = new User(this.value);
|
||||
user[k] = v;
|
||||
this.$emit("input", user);
|
||||
},
|
||||
|
||||
reset () {
|
||||
},
|
||||
|
||||
save () {
|
||||
},
|
||||
|
||||
back () {
|
||||
this.$emit('close');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
97
lib/www/client/source/src/lib/user/User.js
Normal file
97
lib/www/client/source/src/lib/user/User.js
Normal file
@@ -0,0 +1,97 @@
|
||||
|
||||
import { User as BaseUser } from '@dougal/user';
|
||||
|
||||
class User extends BaseUser {
|
||||
|
||||
api // Instance of Vuex api method
|
||||
dirty // Whether the values have changed since last saved
|
||||
|
||||
constructor (data, client) {
|
||||
super (data);
|
||||
|
||||
if (client) {
|
||||
this.api = client;
|
||||
} else if (data instanceof User) {
|
||||
this.api = data.api;
|
||||
}
|
||||
|
||||
this.dirty = false;
|
||||
this.on("changed", () => this.dirty = true);
|
||||
}
|
||||
|
||||
static async fromAPI (api, id) {
|
||||
if (id) {
|
||||
const url = `/user/${id}`;
|
||||
const res = await api([url]);
|
||||
return new User(res, api);
|
||||
} else {
|
||||
const url = `/user`;
|
||||
const res = await api([url]);
|
||||
return res?.map( row => new User(row, api) );
|
||||
}
|
||||
}
|
||||
|
||||
/** Save this user to the server
|
||||
*
|
||||
* If this is a new user, the `api` parameter must be
|
||||
* supplied and this will result in a `POST` request.
|
||||
* For an existing user coming from the database,
|
||||
* `this.api` will be used for a `PUT` request.
|
||||
*/
|
||||
async save (api) {
|
||||
if (this.api) {
|
||||
const url = `/user/${this.id}`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "PUT",
|
||||
body: this.toJSON()
|
||||
};
|
||||
const res = await this.api([url, init]);
|
||||
if (res) {
|
||||
this.dirty = false;
|
||||
return new User(res, this.api);
|
||||
} else {
|
||||
// Something has gone wrong
|
||||
console.log("Something has gone wrong (PUT)");
|
||||
}
|
||||
} else if (api) {
|
||||
const url = `/user`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "POST",
|
||||
body: this.toJSON()
|
||||
}
|
||||
const res = await api([url, init]);
|
||||
if (res) {
|
||||
return new User(res, api);
|
||||
} else {
|
||||
// Something has gone wrong
|
||||
console.log("Something has gone wrong (POST)");
|
||||
}
|
||||
} else {
|
||||
throw new Error("Don't know how to save this user");
|
||||
}
|
||||
}
|
||||
|
||||
/** Delete this user from the server
|
||||
*/
|
||||
async remove () {
|
||||
const url = `/user/${this.id}`;
|
||||
const init = {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
method: "PUT",
|
||||
body: this.toJSON()
|
||||
};
|
||||
const res = await this.api([url, init]);
|
||||
console.log("remove RES", res);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default User;
|
||||
5
lib/www/client/source/src/lib/user/index.js
Normal file
5
lib/www/client/source/src/lib/user/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import User from './User'
|
||||
|
||||
export {
|
||||
User
|
||||
}
|
||||
@@ -47,9 +47,11 @@ new Vue({
|
||||
methods: {
|
||||
|
||||
markdown (value) {
|
||||
return typeof value == "string"
|
||||
? marked(value)
|
||||
: value;
|
||||
return markdown(value);
|
||||
},
|
||||
|
||||
markdownInline (value) {
|
||||
return markdownInline(value);
|
||||
},
|
||||
|
||||
showSnack(text, colour = "primary") {
|
||||
|
||||
35
lib/www/client/source/src/mixins/access.js
Normal file
35
lib/www/client/source/src/mixins/access.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Organisations } from '@dougal/organisations';
|
||||
|
||||
export default {
|
||||
name: "AccessMixin",
|
||||
|
||||
computed: {
|
||||
...mapGetters(['user', 'projectConfiguration'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
access (operation, organisations) {
|
||||
if (this.user) {
|
||||
if (!organisations) organisations = this.projectConfiguration?.organisations;
|
||||
if (!organisations instanceof Organisations) {
|
||||
organisations = new Organisations(organisations);
|
||||
}
|
||||
return this.user.canDo(operation, organisations);
|
||||
}
|
||||
},
|
||||
|
||||
readaccess (item) {
|
||||
return this.access('read', item);
|
||||
},
|
||||
|
||||
writeaccess (item) {
|
||||
return this.access('write', item);
|
||||
},
|
||||
|
||||
adminaccess (item) {
|
||||
return this.access('edit', item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -103,7 +117,9 @@ Vue.use(VueRouter)
|
||||
{ text: "Projects", href: "/projects" },
|
||||
{
|
||||
text: (ctx) => ctx.$store.state.project.projectName || "…",
|
||||
href: (ctx) => `/projects/${ctx.$store.state.project.projectId || ctx.$route.params.project || ""}/`
|
||||
href: (ctx) => `/projects/${ctx.$store.state.project.projectId || ctx.$route.params.project || ""}/`,
|
||||
title: (ctx) => Object.entries(ctx.$store.getters.projectConfiguration?.organisations ?? {}).map( ([org, ops]) => `* ${org}: ${Object.entries(ops).filter( ([k, v]) => v ).map( ([k, v]) => k ).join(", ")}`).join("\n"),
|
||||
organisations: (ctx) => ctx.$store.getters.projectConfiguration?.organisations ?? {}
|
||||
}
|
||||
],
|
||||
appBarExtension: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
async function api ({state, getters, commit, dispatch}, [resource, init = {}, cb]) {
|
||||
async function api ({state, getters, commit, dispatch}, [resource, init = {}, cb, opts = {}]) {
|
||||
try {
|
||||
commit("queueRequest");
|
||||
if (init && init.hasOwnProperty("body")) {
|
||||
@@ -49,7 +49,9 @@ async function api ({state, getters, commit, dispatch}, [resource, init = {}, cb
|
||||
message = body.message;
|
||||
}
|
||||
}
|
||||
await dispatch('showSnack', [message, "warning"]);
|
||||
if (!opts?.silent) {
|
||||
await dispatch('showSnack', [message, "warning"]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && err.name == "AbortError") return;
|
||||
|
||||
@@ -13,7 +13,7 @@ async function refreshProjects ({commit, dispatch, state, rootState}) {
|
||||
const init = {
|
||||
signal: state.loading.signal
|
||||
};
|
||||
const res = await dispatch('api', [url, init]);
|
||||
const res = await dispatch('api', [url, init, null, {silent:true}]);
|
||||
|
||||
if (res) {
|
||||
commit('setProjects', res);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import jwt_decode from 'jwt-decode';
|
||||
import { User } from '@/lib/user';
|
||||
|
||||
async function login ({commit, dispatch}, loginRequest) {
|
||||
const url = "/login";
|
||||
@@ -34,13 +35,13 @@ function cookieChanged (cookie) {
|
||||
return browserCookie != cookie;
|
||||
}
|
||||
|
||||
function setCredentials ({state, commit, getters, dispatch}, {force, token} = {}) {
|
||||
function setCredentials ({state, commit, getters, dispatch, rootState}, {force, token} = {}) {
|
||||
if (token || force || cookieChanged(state.cookie)) {
|
||||
try {
|
||||
const cookie = browserCookie();
|
||||
const decoded = (token ?? cookie) ? jwt_decode(token ?? cookie.split("=")[1]) : null;
|
||||
commit('setCookie', (cookie ?? (token && ("JWT="+token))) || undefined);
|
||||
commit('setUser', decoded);
|
||||
commit('setUser', decoded ? new User(decoded, rootState.api.api) : null);
|
||||
} catch (err) {
|
||||
if (err.name == "InvalidTokenError") {
|
||||
console.warn("Failed to decode", browserCookie());
|
||||
@@ -56,11 +57,11 @@ function setCredentials ({state, commit, getters, dispatch}, {force, token} = {}
|
||||
* Save user preferences to localStorage and store.
|
||||
*
|
||||
* User preferences are identified by a key that gets
|
||||
* prefixed with the user name and role. The value can
|
||||
* prefixed with the user ID. The value can
|
||||
* be anything that JSON.stringify can parse.
|
||||
*/
|
||||
function saveUserPreference ({state, commit}, [key, value]) {
|
||||
const k = `${state.user?.name}.${state.user?.role}.${key}`;
|
||||
const k = `${state.user?.id}.${key}`;
|
||||
|
||||
if (value !== undefined) {
|
||||
localStorage.setItem(k, JSON.stringify(value));
|
||||
@@ -79,7 +80,7 @@ function saveUserPreference ({state, commit}, [key, value]) {
|
||||
|
||||
async function loadUserPreferences ({state, commit}) {
|
||||
// Get all keys which are of interest to us
|
||||
const prefix = `${state.user?.name}.${state.user?.role}`;
|
||||
const prefix = `${state.user?.id}`;
|
||||
const keys = Object.keys(localStorage).filter( k => k.startsWith(prefix) );
|
||||
|
||||
// Build the preferences object
|
||||
|
||||
@@ -9,16 +9,8 @@ function jwt (state) {
|
||||
}
|
||||
}
|
||||
|
||||
function writeaccess (state) {
|
||||
return state.user && ["user", "admin"].includes(state.user.role);
|
||||
}
|
||||
|
||||
function adminaccess (state) {
|
||||
return state.user && state.user.role == "admin";
|
||||
}
|
||||
|
||||
function preferences (state) {
|
||||
return state.preferences;
|
||||
}
|
||||
|
||||
export default { user, jwt, writeaccess, adminaccess, preferences };
|
||||
export default { user, jwt, preferences };
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@input="closeDialog"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-if="writeaccess"
|
||||
<v-btn v-if="writeaccess()"
|
||||
small
|
||||
color="primary"
|
||||
v-bind="attrs"
|
||||
@@ -182,7 +182,7 @@
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn v-if="writeaccess"
|
||||
<v-btn v-if="writeaccess()"
|
||||
small
|
||||
text
|
||||
color="primary"
|
||||
@@ -205,7 +205,7 @@
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="writeaccess"
|
||||
<v-btn v-if="writeaccess()"
|
||||
small
|
||||
dark
|
||||
color="red"
|
||||
@@ -247,7 +247,7 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="writeaccess"
|
||||
<v-btn v-if="writeaccess()"
|
||||
small
|
||||
dark
|
||||
color="red"
|
||||
@@ -303,10 +303,15 @@
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: "Equipment",
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
data () {
|
||||
return {
|
||||
latest: [],
|
||||
@@ -395,7 +400,7 @@ export default {
|
||||
return null;
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'writeaccess', 'loading', 'serverEvent'])
|
||||
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||
|
||||
},
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ export default {
|
||||
return this.sequences[0]?.sequence;
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'preferences', 'writeaccess', 'loading', 'serverEvent'])
|
||||
...mapGetters(['user', 'preferences', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
|
||||
<v-menu v-if="writeaccess"
|
||||
<v-menu v-if="writeaccess()"
|
||||
v-model="contextMenuShow"
|
||||
:position-x="contextMenuX"
|
||||
:position-y="contextMenuY"
|
||||
@@ -164,7 +164,7 @@
|
||||
</v-text-field>
|
||||
<div v-else>
|
||||
<span v-html="$options.filters.markdownInline(item.remarks)"></span>
|
||||
<v-btn v-if="writeaccess && edit === null"
|
||||
<v-btn v-if="writeaccess() && edit === null"
|
||||
icon
|
||||
small
|
||||
title="Edit"
|
||||
@@ -196,6 +196,7 @@
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import DougalLineStatus from '@/components/line-status';
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: "LineList",
|
||||
@@ -204,6 +205,10 @@ export default {
|
||||
DougalLineStatus
|
||||
},
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
data () {
|
||||
return {
|
||||
headers: [
|
||||
@@ -281,7 +286,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['user', 'writeaccess', 'linesLoading', 'lines', 'sequences', 'plannedSequences'])
|
||||
...mapGetters(['user', 'linesLoading', 'lines', 'sequences', 'plannedSequences'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
>mdi-format-list-numbered</v-icon>
|
||||
</a>
|
||||
|
||||
<dougal-event-edit v-if="writeaccess"
|
||||
<dougal-event-edit v-if="$parent.writeaccess()"
|
||||
v-model="eventDialog"
|
||||
v-bind="editedEvent"
|
||||
:available-labels="userLabels"
|
||||
@@ -54,7 +54,7 @@
|
||||
>
|
||||
</dougal-event-edit>
|
||||
|
||||
<dougal-event-edit-labels v-if="writeaccess"
|
||||
<dougal-event-edit-labels v-if="$parent.writeaccess()"
|
||||
v-model="eventLabelsDialog"
|
||||
:labels="userLabels"
|
||||
:selected="contextMenuItem ? contextMenuItem.labels||[] : []"
|
||||
@@ -171,7 +171,7 @@
|
||||
<v-card-text>
|
||||
|
||||
<!-- BEGIN Context menu for log entries -->
|
||||
<v-menu v-if="writeaccess"
|
||||
<v-menu v-if="$parent.writeaccess()"
|
||||
v-model="contextMenuShow"
|
||||
:position-x="contextMenuX"
|
||||
:position-y="contextMenuY"
|
||||
@@ -325,7 +325,7 @@
|
||||
@click="labelSearch=label"
|
||||
>{{label}}</v-chip>
|
||||
</span>
|
||||
<dougal-event-edit-history v-if="entry.has_edits && writeaccess"
|
||||
<dougal-event-edit-history v-if="entry.has_edits && $parent.writeaccess()"
|
||||
:id="entry.id"
|
||||
:disabled="eventsLoading"
|
||||
:labels="labels"
|
||||
@@ -541,7 +541,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'writeaccess', 'eventsLoading', 'online', 'sequence', 'line', 'point', 'position', 'timestamp', 'lineName', 'events', 'labels', 'userLabels', 'projectConfiguration']),
|
||||
...mapGetters(['user', 'eventsLoading', 'online', 'sequence', 'line', 'point', 'position', 'timestamp', 'lineName', 'events', 'labels', 'userLabels', 'projectConfiguration']),
|
||||
...mapState({projectSchema: state => state.project.projectSchema})
|
||||
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
|
||||
<v-menu v-if="writeaccess"
|
||||
<v-menu v-if="writeaccess()"
|
||||
v-model="contextMenuShow"
|
||||
:position-x="contextMenuX"
|
||||
:position-y="contextMenuY"
|
||||
@@ -68,7 +68,7 @@
|
||||
<v-card class="mb-5" flat>
|
||||
<v-card-title class="text-overline">
|
||||
Comments
|
||||
<template v-if="writeaccess">
|
||||
<template v-if="writeaccess()">
|
||||
<v-btn v-if="!editRemarks"
|
||||
class="ml-3"
|
||||
small
|
||||
@@ -131,7 +131,7 @@
|
||||
</template>
|
||||
|
||||
<template v-slot:item.sequence="{item, value}">
|
||||
<v-edit-dialog v-if="writeaccess"
|
||||
<v-edit-dialog v-if="writeaccess()"
|
||||
large
|
||||
@open="editItem(item, 'sequence')"
|
||||
@save="edit = null"
|
||||
@@ -156,7 +156,7 @@
|
||||
</template>
|
||||
|
||||
<template v-slot:item.name="{item, value}">
|
||||
<v-edit-dialog v-if="writeaccess"
|
||||
<v-edit-dialog v-if="writeaccess()"
|
||||
large
|
||||
@open="editItem(item, 'name')"
|
||||
@save="edit = null"
|
||||
@@ -175,7 +175,7 @@
|
||||
</template>
|
||||
|
||||
<template v-slot:item.fsp="{item, value}">
|
||||
<v-edit-dialog v-if="writeaccess"
|
||||
<v-edit-dialog v-if="writeaccess()"
|
||||
large
|
||||
@open="editItem(item, 'fsp')"
|
||||
@save="edit = null"
|
||||
@@ -195,7 +195,7 @@
|
||||
</template>
|
||||
|
||||
<template v-slot:item.lsp="{item, value}">
|
||||
<v-edit-dialog v-if="writeaccess"
|
||||
<v-edit-dialog v-if="writeaccess()"
|
||||
large
|
||||
@open="editItem(item, 'lsp')"
|
||||
@save="edit = null"
|
||||
@@ -215,7 +215,7 @@
|
||||
</template>
|
||||
|
||||
<template v-slot:item.ts0="{item, value}">
|
||||
<v-edit-dialog v-if="writeaccess"
|
||||
<v-edit-dialog v-if="writeaccess()"
|
||||
large
|
||||
@open="editItem(item, 'ts0', item.ts0.toISOString())"
|
||||
@save="edit = null"
|
||||
@@ -235,7 +235,7 @@
|
||||
</template>
|
||||
|
||||
<template v-slot:item.ts1="{item, value}">
|
||||
<v-edit-dialog v-if="writeaccess"
|
||||
<v-edit-dialog v-if="writeaccess()"
|
||||
large
|
||||
@open="editItem(item, 'ts1', item.ts1.toISOString())"
|
||||
@save="edit = null"
|
||||
@@ -263,7 +263,7 @@
|
||||
</template>
|
||||
|
||||
<template v-slot:item.remarks="{item}">
|
||||
<v-text-field v-if="writeaccess && edit && edit.sequence == item.sequence && edit.key == 'remarks'"
|
||||
<v-text-field v-if="writeaccess() && edit && edit.sequence == item.sequence && edit.key == 'remarks'"
|
||||
type="text"
|
||||
v-model="edit.value"
|
||||
prepend-icon="mdi-restore"
|
||||
@@ -275,7 +275,7 @@
|
||||
</v-text-field>
|
||||
<div v-else>
|
||||
<span v-html="$options.filters.markdownInline(item.remarks)"></span>
|
||||
<v-btn v-if="edit === null && writeaccess"
|
||||
<v-btn v-if="edit === null && writeaccess()"
|
||||
icon
|
||||
small
|
||||
title="Edit"
|
||||
@@ -289,7 +289,7 @@
|
||||
</template>
|
||||
|
||||
<template v-slot:item.speed="{item}">
|
||||
<v-edit-dialog v-if="writeaccess"
|
||||
<v-edit-dialog v-if="writeaccess()"
|
||||
large
|
||||
@open="editItem(item, 'speed', knots(item).toFixed(1))"
|
||||
@save="edit = null"
|
||||
@@ -311,7 +311,7 @@
|
||||
</template>
|
||||
|
||||
<template v-slot:item.lag="{item}">
|
||||
<v-edit-dialog v-if="writeaccess"
|
||||
<v-edit-dialog v-if="writeaccess()"
|
||||
large
|
||||
@open="editItem(item, 'lagAfter', Math.round(lagAfter(item)/(60*1000)))"
|
||||
@save="edit = null"
|
||||
@@ -344,6 +344,7 @@
|
||||
<script>
|
||||
import suncalc from 'suncalc';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: "Plan",
|
||||
@@ -351,6 +352,10 @@ export default {
|
||||
components: {
|
||||
},
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
data () {
|
||||
return {
|
||||
headers: [
|
||||
@@ -557,7 +562,7 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['user', 'writeaccess', 'plannedSequencesLoading', 'plannedSequences', 'planRemarks'])
|
||||
...mapGetters(['user', 'plannedSequencesLoading', 'plannedSequences', 'planRemarks'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
@@ -20,9 +20,15 @@
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: 'Project',
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
components: {
|
||||
},
|
||||
data () {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
>mdi-archive-outline</v-icon>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a :href="`/projects/${item.pid}`">{{value}}</a>
|
||||
<a :href="`/projects/${item.pid}`" :title="title(item)">{{value}}</a>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
</v-data-table>
|
||||
|
||||
<v-menu v-if="adminaccess"
|
||||
<v-menu v-if="adminaccess(contextMenuItem)"
|
||||
v-model="contextMenuShow"
|
||||
:position-x="contextMenuX"
|
||||
:position-y="contextMenuY"
|
||||
@@ -106,6 +106,7 @@ td p:last-of-type {
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import DougalProjectSettingsNameIdRootpath from '@/components/project-settings/name-id-rootpath'
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: "ProjectList",
|
||||
@@ -114,6 +115,10 @@ export default {
|
||||
DougalProjectSettingsNameIdRootpath
|
||||
},
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
data () {
|
||||
return {
|
||||
headers: [
|
||||
@@ -179,7 +184,7 @@ export default {
|
||||
: this.items.filter(i => !i.archived);
|
||||
},
|
||||
|
||||
...mapGetters(['loading', 'serverEvent', 'adminaccess', 'projects'])
|
||||
...mapGetters(['loading', 'serverEvent', 'projects'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
@@ -205,6 +210,15 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
title (item) {
|
||||
if (item.organisations) {
|
||||
return "Access:\n" + Object.entries(item.organisations).map( org =>
|
||||
`• ${org[0]} (${Object.entries(org[1]).filter( access => access[1] ).map( access => access[0] ).join(", ")})`
|
||||
).join("\n")
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
async load () {
|
||||
await this.list();
|
||||
const promises = [];
|
||||
|
||||
@@ -228,6 +228,7 @@ import { deepSet } from '@/lib/utils';
|
||||
import DougalJsonBuilder from '@/components/json-builder/json-builder';
|
||||
|
||||
import DougalProjectSettingsNameId from '@/components/project-settings/name-id';
|
||||
import DougalProjectSettingsOrganisations from '@/components/project-settings/organisations';
|
||||
import DougalProjectSettingsGroups from '@/components/project-settings/groups';
|
||||
import DougalProjectSettingsGeodetics from '@/components/project-settings/geodetics';
|
||||
import DougalProjectSettingsBinning from '@/components/project-settings/binning';
|
||||
@@ -248,6 +249,7 @@ import DougalProjectSettingsNotImplemented from '@/components/project-settings/n
|
||||
|
||||
const components = {
|
||||
name_id: DougalProjectSettingsNameId,
|
||||
organisations: DougalProjectSettingsOrganisations,
|
||||
groups: DougalProjectSettingsGroups,
|
||||
geodetics: DougalProjectSettingsGeodetics,
|
||||
binning: DougalProjectSettingsBinning,
|
||||
@@ -304,6 +306,13 @@ export default {
|
||||
name: obj?.name
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "organisations",
|
||||
name: "Organisations",
|
||||
values: (obj) => ({
|
||||
organisations: obj?.organisations
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "groups",
|
||||
name: "Groups",
|
||||
@@ -571,7 +580,7 @@ export default {
|
||||
return messages;
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'writeaccess', 'loading', 'serverEvent'])
|
||||
...mapGetters(['user', 'loading', 'serverEvent'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
@@ -82,13 +82,13 @@
|
||||
small
|
||||
:color="labels[label] && labels[label].view.colour"
|
||||
:title="labels[label] && labels[label].view.description"
|
||||
:close="writeaccess && label == 'QCAccepted'"
|
||||
:close="writeaccess() && label == 'QCAccepted'"
|
||||
@click:close="unaccept(item)"
|
||||
>
|
||||
{{label}}
|
||||
</v-chip>
|
||||
|
||||
<dougal-qc-acceptance v-if="writeaccess"
|
||||
<dougal-qc-acceptance v-if="writeaccess()"
|
||||
:item="item"
|
||||
@accept="accept"
|
||||
@unaccept="unaccept"
|
||||
@@ -105,7 +105,7 @@
|
||||
>
|
||||
</v-chip>
|
||||
|
||||
<dougal-qc-acceptance v-if="writeaccess"
|
||||
<dougal-qc-acceptance v-if="writeaccess()"
|
||||
:item="item"
|
||||
@accept="accept"
|
||||
@unaccept="unaccept"
|
||||
@@ -113,7 +113,7 @@
|
||||
|
||||
</div>
|
||||
<div class="text--secondary" v-else>
|
||||
<dougal-qc-acceptance v-if="writeaccess"
|
||||
<dougal-qc-acceptance v-if="writeaccess()"
|
||||
:item="item"
|
||||
@accept="accept"
|
||||
@unaccept="unaccept"
|
||||
@@ -140,6 +140,7 @@
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { withParentProps } from '@/lib/utils';
|
||||
import DougalQcAcceptance from '@/components/qc-acceptance';
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: "QC",
|
||||
@@ -148,6 +149,10 @@ export default {
|
||||
DougalQcAcceptance
|
||||
},
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
data () {
|
||||
return {
|
||||
updatedOn: null,
|
||||
@@ -227,7 +232,7 @@ export default {
|
||||
return values;
|
||||
},
|
||||
|
||||
...mapGetters(['writeaccess', 'loading'])
|
||||
...mapGetters(['loading'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
offset-y
|
||||
>
|
||||
<v-list dense v-if="contextMenuItem">
|
||||
<v-list-item @click="addToPlan(false); contextMenuShow=false" v-if="writeaccess">
|
||||
<v-list-item @click="addToPlan(false); contextMenuShow=false" v-if="writeaccess()">
|
||||
<v-list-item-title>Reshoot</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="addToPlan(true); contextMenuShow=false" v-if="writeaccess">
|
||||
<v-list-item @click="addToPlan(true); contextMenuShow=false" v-if="writeaccess()">
|
||||
<v-list-item-title>Reshoot with overlap</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
<!-- Item is not in queue -->
|
||||
<v-list-item
|
||||
v-if="writeaccess && !contextMenuItemInTransferQueue"
|
||||
v-if="writeaccess() && !contextMenuItemInTransferQueue"
|
||||
@click="addToTransferQueue(); contextMenuShow=false"
|
||||
>
|
||||
<v-list-item-content>
|
||||
@@ -97,7 +97,7 @@
|
||||
</v-list-item>
|
||||
<!-- Item queued, not yet sent -->
|
||||
<v-list-item two-line
|
||||
v-else-if="writeaccess && contextMenuItemInTransferQueue.status == 'queued'"
|
||||
v-else-if="writeaccess() && contextMenuItemInTransferQueue.status == 'queued'"
|
||||
@click="removeFromTransferQueue(); contextMenuShow=false"
|
||||
>
|
||||
<v-list-item-content>
|
||||
@@ -112,7 +112,7 @@
|
||||
</v-list-item>
|
||||
<!-- Item already sent -->
|
||||
<v-list-item two-line
|
||||
v-else-if="writeaccess && contextMenuItemInTransferQueue.status == 'sent'"
|
||||
v-else-if="writeaccess() && contextMenuItemInTransferQueue.status == 'sent'"
|
||||
@click="addToTransferQueue(); contextMenuShow=false"
|
||||
>
|
||||
<v-list-item-content>
|
||||
@@ -127,7 +127,7 @@
|
||||
</v-list-item>
|
||||
<!-- Item sending was cancelled -->
|
||||
<v-list-item two-line
|
||||
v-else-if="writeaccess && contextMenuItemInTransferQueue.status == 'cancelled'"
|
||||
v-else-if="writeaccess() && contextMenuItemInTransferQueue.status == 'cancelled'"
|
||||
@click="addToTransferQueue(); contextMenuShow=false"
|
||||
>
|
||||
<v-list-item-content>
|
||||
@@ -170,7 +170,7 @@
|
||||
<v-card outlined class="flex-grow-1">
|
||||
<v-card-title>
|
||||
Acquisition remarks
|
||||
<template v-if="writeaccess">
|
||||
<template v-if="writeaccess()">
|
||||
<template v-if="edit && edit.sequence == item.sequence && edit.key == 'remarks'">
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
@@ -222,7 +222,7 @@
|
||||
<v-card outlined class="flex-grow-1" v-if="item.remarks_final !== null">
|
||||
<v-card-title>
|
||||
Processing remarks
|
||||
<template v-if="writeaccess">
|
||||
<template v-if="writeaccess()">
|
||||
<template v-if="edit && edit.sequence == item.sequence && edit.key == 'remarks_final'">
|
||||
<v-btn
|
||||
class="ml-3"
|
||||
@@ -492,10 +492,15 @@ tr :nth-child(5), tr :nth-child(8), tr :nth-child(11), tr :nth-child(14) {
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { basename } from 'path';
|
||||
import throttle from '@/lib/throttle';
|
||||
import AccessMixin from '@/mixins/access';
|
||||
|
||||
export default {
|
||||
name: "SequenceList",
|
||||
|
||||
mixins: [
|
||||
AccessMixin
|
||||
],
|
||||
|
||||
data () {
|
||||
return {
|
||||
headers: [
|
||||
@@ -616,7 +621,7 @@ export default {
|
||||
return this.queuedItems.find(i => i.payload.sequence == this.contextMenuItem.sequence);
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'writeaccess', 'sequencesLoading', 'sequences'])
|
||||
...mapGetters(['user', 'sequencesLoading', 'sequences'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
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