Mercurial > gemma
changeset 2501:9d9c6425db82 critical-bottlenecks
merged default into critical-bottlenecks branch
author | Markus Kottlaender <markus@intevation.de> |
---|---|
date | Mon, 04 Mar 2019 16:01:20 +0100 |
parents | 7247eb03e7c0 (current diff) 8cc3cd1b27f2 (diff) |
children | e13daf439068 |
files | |
diffstat | 9 files changed, 406 insertions(+), 526 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/assets/application.scss Mon Mar 04 08:32:05 2019 +0100 +++ b/client/src/assets/application.scss Mon Mar 04 16:01:20 2019 +0100 @@ -145,3 +145,22 @@ .btn.disabled { opacity: 0.4; } + +.snotifyToast { + text-align: left; + border-radius: 0.25rem; + box-shadow: 0 0.1rem 0.5rem rgba(0, 0, 0, 0.2); + border-left: 0 !important; + &.snotify-info { + border-top: 4px solid $color-info; + .snotify-icon--info { + background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20version=%221.1%22%20x=%220px%22%20y=%220px%22%20viewBox=%220%200%20512%20512%22%20fill=%22%2317a2b8%22%3E%3Cg%3E%3Cpath%20d=%22M256,0C114.84,0,0,114.84,0,256S114.84,512,256,512,512,397.16,512,256,397.15,0,256,0Zm0,478.43C133.35,478.43,33.57,378.64,33.57,256S133.35,33.58,256,33.58,478.42,133.36,478.42,256,378.64,478.43,256,478.43Z%22/%3E%3Cpath%20d=%22M251.26,161.24a22.39,22.39,0,1,0-22.38-22.39A22.39,22.39,0,0,0,251.26,161.24Z%22/%3E%3Cpath%20d=%22M286.84,357.87h-14v-160A16.79,16.79,0,0,0,256,181.05H225.17a16.79,16.79,0,0,0,0,33.58h14.05V357.87H225.17a16.79,16.79,0,0,0,0,33.57h61.67a16.79,16.79,0,1,0,0-33.57Z%22/%3E%3C/g%3E%3C/svg%3E"); + } + } + &.snotify-error { + border-top: 4px solid #f44336; + } + .snotifyToast__title { + font-size: 1.2rem; + } +}
--- a/client/src/components/Bottlenecks.vue Mon Mar 04 08:32:05 2019 +0100 +++ b/client/src/components/Bottlenecks.vue Mon Mar 04 16:01:20 2019 +0100 @@ -5,123 +5,109 @@ title="Bottlenecks" :closeCallback="$parent.close" /> - <div class="row p-2 text-left small"> - <div class="col-5"> - <a href="#" @click="sortBy('name')" class="sort-link"> - <translate>Name</translate> - </a> - <font-awesome-icon - :icon="sortIcon" - class="ml-1" - v-if="sortColumn === 'name'" - ></font-awesome-icon> + <UITableHeader + :columns="[ + { id: 'name', title: 'Name', class: 'col-4' }, + { + id: 'latestMeasurement', + title: 'Latest Measurement', + class: 'col-3' + }, + { id: 'chainage', title: 'Chainage', class: 'col-3' } + ]" + @sortingChanged="sortBy" + /> + <UITableBody + :data="filteredAndSortedBottlenecks()" + :maxHeight="(showSplitscreen ? 18 : 35) + 'rem'" + :active="openBottleneck" + v-slot="{ item: bottleneck }" + > + <div class="col-4 py-2 text-left"> + <a href="#" @click="selectBottleneck(bottleneck)">{{ + bottleneck.properties.name + }}</a> </div> - <div class="col-2"> - <a href="#" @click="sortBy('latestMeasurement')" class="sort-link"> - <translate>Latest</translate> <br /> - <translate>Measurement</translate> - </a> - <font-awesome-icon - :icon="sortIcon" - class="ml-1" - v-if="sortColumn === 'latestMeasurement'" - ></font-awesome-icon> + <div class="col-3 py-2"> + {{ formatSurveyDate(bottleneck.properties.current) }} + </div> + <div class="col-3 py-2"> + {{ + displayCurrentChainage( + bottleneck.properties.from, + bottleneck.properties.to + ) + }} </div> - <div class="col-3"> - <a href="#" @click="sortBy('chainage')" class="sort-link"> - <translate>Chainage</translate> + <div class="col-2 pr-0 text-right d-flex flex-column"> + <a + class="text-info mt-auto mb-auto mr-2" + @click="loadSurveys(bottleneck)" + v-if="bottleneck.properties.current" + > + <font-awesome-icon + class="pointer" + icon="spinner" + fixed-width + spin + v-if="loading === bottleneck" + ></font-awesome-icon> + <font-awesome-icon + class="pointer" + icon="angle-down" + fixed-width + v-if="loading !== bottleneck && openBottleneck !== bottleneck" + ></font-awesome-icon> + <font-awesome-icon + class="pointer" + icon="angle-up" + fixed-width + v-if="loading !== bottleneck && openBottleneck === bottleneck" + ></font-awesome-icon> </a> - <font-awesome-icon - :icon="sortIcon" - class="ml-1" - v-if="sortColumn === 'chainage'" - ></font-awesome-icon> </div> - <div class="col-2"></div> - </div> - <div - class="bottleneck-list small text-left" - :style="'max-height: ' + (showSplitscreen ? 18 : 35) + 'rem'" - v-if="filteredAndSortedBottlenecks().length" - > <div - v-for="bottleneck in filteredAndSortedBottlenecks()" - :key="bottleneck.properties.name" - class="border-top row bottleneck-row mx-0" + :class="[ + 'col-12 p-0', + 'surveys', + { open: openBottleneck === bottleneck } + ]" > - <div class="col-5 py-2 text-left"> - <a href="#" @click="selectBottleneck(bottleneck)">{{ - bottleneck.properties.name - }}</a> - </div> - <div class="col-2 py-2"> - {{ formatSurveyDate(bottleneck.properties.current) }} - </div> - <div class="col-3 py-2"> - {{ - displayCurrentChainage( - bottleneck.properties.from, - bottleneck.properties.to - ) - }} - </div> - <div class="col-2 pr-0 text-right d-flex flex-column"> - <a - class="text-info mt-auto mb-auto mr-2" - @click="loadSurveys(bottleneck.properties.name)" - v-if="bottleneck.properties.current" - > - <font-awesome-icon - class="pointer" - icon="spinner" - fixed-width - spin - v-if="loading === bottleneck.properties.name" - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - icon="angle-down" - fixed-width - v-if=" - loading !== bottleneck.properties.name && - openBottleneck !== bottleneck.properties.name - " - ></font-awesome-icon> - <font-awesome-icon - class="pointer" - icon="angle-up" - fixed-width - v-if=" - loading !== bottleneck.properties.name && - openBottleneck === bottleneck.properties.name - " - ></font-awesome-icon> - </a> - </div> - <div - :class="[ - 'col-12 p-0', - 'surveys', - { open: openBottleneck === bottleneck.properties.name } - ]" + <a + href="#" + class="d-inline-block px-3 py-2" + v-for="(survey, index) in openBottleneckSurveys" + :key="index" + @click="selectSurvey(survey, bottleneck)" > - <a - href="#" - class="d-block px-3 py-2" - v-for="(survey, index) in openBottleneckSurveys" - :key="index" - @click="selectSurvey(survey, bottleneck)" - >{{ formatSurveyDate(survey.date_info) }}</a - > - </div> + {{ formatSurveyDate(survey.date_info) }} + </a> </div> - </div> - <div v-else class="small text-center py-3 border-top"> - <translate>No results.</translate> - </div> + </UITableBody> </div> </template> +<style lang="sass" scoped> +.table-body + .row + > div:not(:last-child) + transition: background-color 0.3s, color 0.3s + &.active + > div:not(:last-child) + background-color: $color-info + color: #fff + a + color: #fff !important + .surveys + border-bottom: solid 1px $color-info + .surveys + overflow: hidden + max-height: 0 + &.open + overflow-y: auto + max-height: 5rem +</style> + <script> /* This is Free Software under GNU Affero General Public License v >= 3.0 * without warranty, see README.md and license for details. @@ -249,19 +235,18 @@ }); }); }, - sortBy(column) { - this.sortColumn = column; - this.sortDirection = this.sortDirection === "ASC" ? "DESC" : "ASC"; + sortBy(sorting) { + this.sortColumn = sorting.sortColumn; + this.sortDirection = sorting.sortDirection; }, - loadSurveys(name) { - this.openBottleneckSurveys = null; - if (name === this.openBottleneck) { + loadSurveys(bottleneck) { + if (bottleneck === this.openBottleneck) { this.openBottleneck = null; + this.openBottleneckSurveys = null; } else { - this.openBottleneck = name; - this.loading = name; + this.loading = bottleneck; - HTTP.get("/surveys/" + name, { + HTTP.get("/surveys/" + bottleneck.properties.name, { headers: { "X-Gemma-Auth": localStorage.getItem("token"), "Content-type": "text/xml; charset=UTF-8" @@ -271,6 +256,7 @@ this.openBottleneckSurveys = response.data.surveys.sort((a, b) => { return a.date_info < b.date_info ? 1 : -1; }); + this.openBottleneck = bottleneck; }) .catch(error => { const { status, data } = error.response; @@ -291,37 +277,3 @@ } }; </script> - -<style lang="scss" scoped> -.bottleneck-list { - overflow-y: auto; -} - -.bottleneck-list .bottleneck-row a { - text-decoration: none; -} - -.bottleneck-list .bottleneck-row:hover { - background: #fbfbfb; -} - -.surveys { - max-height: 0; - min-height: 0; - overflow: hidden; -} - -.surveys a:hover { - background: #f3f3f3; -} - -.surveys.open { - max-height: 250px; - overflow: auto; -} - -.sort-link { - color: #444; - font-weight: bold; -} -</style>
--- a/client/src/components/ImportStretches.vue Mon Mar 04 08:32:05 2019 +0100 +++ b/client/src/components/ImportStretches.vue Mon Mar 04 16:01:20 2019 +0100 @@ -5,60 +5,52 @@ title="Define Stretches" :closeCallback="$parent.close" /> - <div v-if="!edit" class="mb-3 mr-3 ml-3 text-left"> - <table v-if="stretches.length > 0" class="table"> - <thead> - <tr> - <th class="header"><translate>Name</translate></th> - <th class="header"><translate>Datum</translate></th> - <th class="header"><translate>Source organization</translate></th> - <th></th> - </tr> - </thead> - <tbody> - <tr class="small" v-for="(stretch, index) in stretches" :key="index"> - <td class=""> - <a - class="linkto text-info" - v-if="isInStaging(stretch.properties.name)" - @click="gotoStaging(getStagingLink(stretch.properties.name))" - > - {{ stretch.properties.name - }}<font-awesome-icon - class="ml-1 text-danger" - icon="exclamation-triangle" - fixed-width - ></font-awesome-icon - ><small class="ml-1">review</small> - </a> - <a v-else @click="moveMapToStretch(index)" href="#">{{ - stretch.properties.name - }}</a> - </td> - <td class=""> - {{ formatSurveyDate(stretch.properties["date_info"]) }} - </td> - <td>{{ stretch.properties["source_organization"] }}</td> - <td class="text-right"> - <button - class="btn btn-sm btn-dark mr-1" - @click="editStretch(index)" - > - <font-awesome-icon icon="pencil-alt" fixed-width /> - </button> - <button - class="btn btn-sm btn-dark" - @click="deleteStretch(stretch)" - > - <font-awesome-icon icon="trash" fixed-width /> - </button> - </td> - </tr> - </tbody> - </table> - <div class="mt-3" v-if="stretches.length == 0"> - <translate>No results.</translate> - </div> + <div v-if="!edit" class="mb-3"> + <UITableHeader + :columns="[ + { id: 'name', title: 'Name', class: 'col-4' }, + { id: 'date', title: 'Date', class: 'col-2' }, + { id: 'srcorg', title: 'Source organization', class: 'col-3' } + ]" + :sortable="false" + /> + <UITableBody :data="stretches" v-slot="{ item: stretch }"> + <div class="py-2 col-4 "> + <a + class="linkto text-info" + v-if="isInStaging(stretch.properties.name)" + @click="gotoStaging(getStagingLink(stretch.properties.name))" + > + {{ stretch.properties.name + }}<font-awesome-icon + class="ml-1 text-danger" + icon="exclamation-triangle" + fixed-width + ></font-awesome-icon + ><small class="ml-1">review</small> + </a> + <a v-else @click="moveMapToStretch(stretch)" href="#">{{ + stretch.properties.name + }}</a> + </div> + <div class="py-2 col-2"> + {{ formatSurveyDate(stretch.properties["date_info"]) }} + </div> + <div class="py-2 col-3"> + {{ stretch.properties["source_organization"] }} + </div> + <div class="py-2 col text-right"> + <button + class="btn btn-sm btn-dark mr-1" + @click="editStretch(stretch)" + > + <font-awesome-icon icon="pencil-alt" fixed-width /> + </button> + <button class="btn btn-sm btn-dark" @click="deleteStretch(stretch)"> + <font-awesome-icon icon="trash" fixed-width /> + </button> + </div> + </UITableBody> </div> <div v-if="edit"> <div class="ml-3 mr-3"> @@ -337,8 +329,8 @@ }); }); }, - editStretch(index) { - const properties = this.stretches[index].properties; + editStretch(stretch) { + const properties = stretch.properties; this.date_info = properties.date_info.split("T")[0]; this.id = properties.name; this.nobjbn = properties.nobjnam; @@ -376,10 +368,10 @@ } }); }, - moveMapToStretch(index) { + moveMapToStretch(stretch) { this.$store.commit("map/setLayerVisible", LAYERS.STRETCHES); this.$store.commit("map/moveToExtent", { - feature: this.stretches[index], + feature: stretch, zoom: 17, preventZoomOut: true });
--- a/client/src/components/fairway/Fairwayprofile.vue Mon Mar 04 08:32:05 2019 +0100 +++ b/client/src/components/fairway/Fairwayprofile.vue Mon Mar 04 16:01:20 2019 +0100 @@ -1,12 +1,22 @@ <template> <div :class="['position-relative', { show: showSplitscreen }]"> <div class="profile bg-white position-relative d-flex flex-column"> - <div class="d-flex flex-row justify-content-between border-bottom"> - <div class="mt-1 mb-1 d-flex flex-row"> - <small class="text-muted ml-1 mr-1 my-auto text-right" - ><translate>Available Waterlevels</translate></small + <div + class="d-flex flex-row justify-content-between align-items-center border-bottom position-relative" + > + <div class="d-flex flex-row align-items-center position-absolute"> + <small class="text-muted px-2 text-right" style="line-height: 1rem;"> + <translate v-if="availableWaterlevels.length > 1" + >Available Waterlevels:</translate + > + <translate v-else>Waterlevel:</translate> + </small> + <select + class="form-control pl-1" + v-model="currentLevel" + v-if="availableWaterlevels.length > 1" + style="width: 100px; height: 30px; font-size: 80%;" > - <select class="form-control" v-model="currentLevel"> <option v-for="level in availableWaterlevels" :value="level" @@ -15,6 +25,7 @@ {{ formatSurveyDate(level) }} </option> </select> + <small v-else>{{ formatSurveyDate(currentLevel) }}</small> </div> <div class="flex-row mr-auto ml-auto"> <h5
--- a/client/src/components/importschedule/Importschedule.vue Mon Mar 04 08:32:05 2019 +0100 +++ b/client/src/components/importschedule/Importschedule.vue Mon Mar 04 16:01:20 2019 +0100 @@ -4,100 +4,81 @@ <div class="mt-3 w-100"> <div class="card flex-grow-1 schedulecard shadow-xs"> <UIBoxHeader icon="clock" title="Imports" /> - <div class="card-body schedulecardbody"> - <div class="card-body schedulecardbody"> - <div class="searchandfilter mb-3 w-50 d-flex flex-row"> - <div class="searchgroup input-group"> - <div class="input-group-prepend"> - <span class="input-group-text" id="search"> - <font-awesome-icon icon="search"></font-awesome-icon> - </span> - </div> - <input - v-model="searchQuery" - type="text" - class="form-control" - placeholder - aria-label="Search" - aria-describedby="search" - /> - </div> + <div class="searchandfilter p-3 w-50 mx-auto"> + <div class="searchgroup input-group"> + <div class="input-group-prepend"> + <span class="input-group-text" id="search"> + <font-awesome-icon icon="search"></font-awesome-icon> + </span> </div> - <transition name="fade"> - <table v-if="schedules.length" class="table table-hover"> - <thead> - <tr> - <th><translate>ID</translate></th> - <th><translate>Type</translate></th> - <th><translate>Author</translate></th> - <th><translate>Schedule</translate></th> - <th><translate>Email</translate></th> - <th style="width: 140px"></th> - </tr> - </thead> - <transition-group name="fade" tag="tbody"> - <tr v-for="schedule in schedules" :key="schedule.id"> - <td>{{ schedule.id }}</td> - <td>{{ schedule.kind.toUpperCase() }}</td> - <td>{{ schedule.user }}</td> - <td>{{ schedule.config.cron }}</td> - <td> - <font-awesome-icon - v-if="schedule.config['send-email']" - class="fa-fw mr-2" - fixed-width - icon="check" - ></font-awesome-icon> - </td> - <td class="text-right"> - <button - @click="editSchedule(schedule.id)" - class="btn btn-sm btn-dark mr-1" - :disabled="importScheduleDetailVisible" - > - <font-awesome-icon - icon="pencil-alt" - fixed-width - ></font-awesome-icon> - </button> - <button - @click="deleteSchedule(schedule)" - class="btn btn-sm btn-dark mr-1" - :disabled="importScheduleDetailVisible" - > - <font-awesome-icon - icon="trash" - fixed-width - ></font-awesome-icon> - </button> - <button - @click="triggerManualImport(schedule.id)" - class="btn btn-sm btn-dark" - :disabled="importScheduleDetailVisible" - > - <font-awesome-icon - icon="play" - fixed-width - ></font-awesome-icon> - </button> - </td> - </tr> - </transition-group> - </table> - <div v-else class="mt-4 small text-center py-3"> - <translate>No scheduled imports</translate> - </div> - </transition> - <div class="text-right"> - <button - :disabled="importScheduleDetailVisible" - @click="newImport" - class="btn btn-info newbutton" - > - <translate>New Import</translate> - </button> - </div> + <input + v-model="searchQuery" + type="text" + class="form-control" + placeholder + aria-label="Search" + aria-describedby="search" + /> + </div> + </div> + <UITableHeader + :columns="[ + { id: 'id', title: 'ID', class: 'col-1' }, + { id: 'type', title: 'Type', class: 'col-2' }, + { id: 'author', title: 'Author', class: 'col-2' }, + { id: 'schedule', title: 'Schedule', class: 'col-2' }, + { id: 'email', title: 'Email', class: 'col-2' } + ]" + :sortable="false" + /> + <UITableBody :data="schedules" v-slot="{ item: schedule }"> + <div class="py-2 col-1">{{ schedule.id }}</div> + <div class="py-2 col-2">{{ schedule.kind.toUpperCase() }}</div> + <div class="py-2 col-2">{{ schedule.user }}</div> + <div class="py-2 col-2">{{ schedule.config.cron }}</div> + <div class="py-2 col-2"> + <font-awesome-icon + v-if="schedule.config['send-email']" + class="fa-fw mr-2" + fixed-width + icon="check" + ></font-awesome-icon> </div> + <div class="py-2 col text-right"> + <button + @click="editSchedule(schedule.id)" + class="btn btn-sm btn-dark mr-1" + :disabled="importScheduleDetailVisible" + > + <font-awesome-icon + icon="pencil-alt" + fixed-width + ></font-awesome-icon> + </button> + <button + @click="deleteSchedule(schedule)" + class="btn btn-sm btn-dark mr-1" + :disabled="importScheduleDetailVisible" + > + <font-awesome-icon icon="trash" fixed-width></font-awesome-icon> + </button> + <button + @click="triggerManualImport(schedule.id)" + class="btn btn-sm btn-dark" + :disabled="importScheduleDetailVisible" + > + <font-awesome-icon icon="play" fixed-width></font-awesome-icon> + </button> + </div> + </UITableBody> + <div class="p-3 text-right"> + <button + :disabled="importScheduleDetailVisible" + @click="newImport" + class="btn btn-info newbutton" + > + <translate>New Import</translate> + </button> </div> </div> </div>
--- a/client/src/components/systemconfiguration/PDFTemplates.vue Mon Mar 04 08:32:05 2019 +0100 +++ b/client/src/components/systemconfiguration/PDFTemplates.vue Mon Mar 04 16:01:20 2019 +0100 @@ -11,44 +11,28 @@ /> </div> <div class="mt-1 border-bottom pb-4"> - <transition name="fade"> - <table class="table table-sm table-hover" v-if="templates.length"> - <thead> - <tr> - <th><translate>Name</translate></th> - <th><translate>Date</translate></th> - <th><translate>Country</translate></th> - <th></th> - </tr> - </thead> - <transition-group name="fade" tag="tbody"> - <tr v-for="template in templates" :key="template.name"> - <td>{{ template.name }}</td> - <td>{{ template.time }}</td> - <td v-if="template.country">{{ template.country }}</td> - <td v-else><i>global</i></td> - <td class="text-right"> - <button - class="btn btn-sm btn-dark" - @click=" - deleteTemplate(template); - showSuccessUploadMsg = false; - " - > - <font-awesome-icon icon="trash" /> - </button> - </td> - </tr> - </transition-group> - </table> - </transition> - <button - class="btn btn-info mt-2" - @click=" - $refs.uploadTemplate.click(); - showSuccessUploadMsg = false; - " - > + <UITableHeader + :columns="[ + { id: 'name', title: 'Name', class: 'col-4' }, + { id: 'date', title: 'Date', class: 'col-4' }, + { id: 'country', title: 'Country', class: 'col-2' } + ]" + :sortable="false" + /> + <UITableBody :data="templates" v-slot="{ item: template }"> + <div class="py-2 col-4">{{ template.name }}</div> + <div class="py-2 col-4">{{ template.time }}</div> + <div class="py-2 col-2" v-if="template.country"> + {{ template.country }} + </div> + <div class="py-2 col-2" v-else><i>global</i></div> + <div class="col py-2 text-right"> + <button class="btn btn-sm btn-dark" @click="deleteTemplate(template)"> + <font-awesome-icon icon="trash" /> + </button> + </div> + </UITableBody> + <button class="btn btn-info mt-2" @click="$refs.uploadTemplate.click()"> <font-awesome-icon icon="spinner" class="fa-spin fa-fw" @@ -57,11 +41,6 @@ <font-awesome-icon icon="upload" class="fa-fw" v-else /> <translate>Upload new template</translate> </button> - <div v-if="showSuccessUploadMsg" class="text-center"> - <p class="text-muted" v-translate> - {{ templateToUpload.name }} uploaded successfully - </p> - </div> </div> </div> </template> @@ -91,16 +70,14 @@ * Markus Kottländer <markus@intevation.de> */ import { HTTP } from "@/lib/http"; -import { displayError } from "@/lib/errors.js"; +import { displayError, displayInfo } from "@/lib/errors.js"; export default { name: "pdftemplates", data() { return { templates: [], - uploading: false, - templateToUpload: "", - showSuccessUploadMsg: false + uploading: false }; }, methods: { @@ -135,8 +112,10 @@ ) .then(() => { this.loadTemplates(); - this.templateToUpload = template; - this.showSuccessUploadMsg = true; + displayInfo({ + message: + template.name + " " + this.$gettext("uploaded successfully") + }); }) .catch(e => { const { status, data } = e.response; @@ -204,6 +183,10 @@ ); if (removeIndex !== -1) { this.templates.splice(removeIndex, 1); + displayInfo({ + message: + template.name + " " + this.$gettext("deleted successfully") + }); } }); }
--- a/client/src/components/ui/UITableBody.vue Mon Mar 04 08:32:05 2019 +0100 +++ b/client/src/components/ui/UITableBody.vue Mon Mar 04 16:01:20 2019 +0100 @@ -8,7 +8,7 @@ > <div v-for="(item, index) in data" - :key="index" + :key="key(index)" :class="['border-top row mx-0', { active: active === item }]" > <slot :item="item" :index="index"></slot> @@ -46,6 +46,11 @@ active: { type: [Object, Array] } + }, + methods: { + key(index) { + return index; + } } }; </script>
--- a/client/src/components/usermanagement/Usermanagement.vue Mon Mar 04 08:32:05 2019 +0100 +++ b/client/src/components/usermanagement/Usermanagement.vue Mon Mar 04 16:01:20 2019 +0100 @@ -5,82 +5,55 @@ <div :class="userlistStyle"> <div class="card shadow-xs"> <UIBoxHeader icon="users-cog" title="Users" /> - <div class="card-body"> - <table id="datatable" :class="tableStyle"> - <thead> - <tr> - <th scope="col" @click="sortBy('role')"> - <span - >Role - <font-awesome-icon - v-if="sortCriterion == 'role'" - icon="angle-down" - ></font-awesome-icon> - </span> - </th> - <th scope="col" @click="sortBy('user')"> - <span - >Username - <font-awesome-icon - v-if="sortCriterion == 'user'" - icon="angle-down" - ></font-awesome-icon> - </span> - </th> - <th scope="col" @click="sortBy('country')"> - <span - >Country - <font-awesome-icon - v-if="sortCriterion == 'country'" - icon="angle-down" - ></font-awesome-icon> - </span> - </th> - <th scope="col" @click="sortBy('email')"> - <span - >Email - <font-awesome-icon - v-if="sortCriterion == 'email'" - icon="angle-down" - ></font-awesome-icon> - </span> - </th> - <th scope="col"></th> - </tr> - </thead> - <transition-group name="fade" tag="tbody"> - <tr v-for="user in users" :key="user.user"> - <td @click="selectUser(user.user)"> - <font-awesome-icon - v-tooltip="roleLabel(user.role)" - :icon="roleIcon(user.role)" - class="fa-lg" - ></font-awesome-icon> - </td> - <td @click="selectUser(user.user)">{{ user.user }}</td> - <td @click="selectUser(user.user)">{{ user.country }}</td> - <td @click="selectUser(user.user)">{{ user.email }}</td> - <td class="text-right"> - <button - @click="sendTestMail(user.user)" - class="btn btn-sm btn-dark mr-1" - v-tooltip="$gettext('Send testmail')" - v-if="user.email" - > - <font-awesome-icon icon="paper-plane"></font-awesome-icon> - </button> - <button - @click="deleteUser(user.user)" - class="btn btn-sm btn-dark" - v-tooltip="$gettext('Delete user')" - > - <font-awesome-icon icon="trash" /> - </button> - </td> - </tr> - </transition-group> - </table> - </div> + <UITableHeader + :columns="[ + { id: 'role', title: 'Role', class: 'col-1' }, + { id: 'user', title: 'Username', class: 'col-3' }, + { id: 'country', title: 'Country', class: 'col-2' }, + { id: 'email', title: 'Email', class: 'col-3' } + ]" + @sortingChanged="sortBy" + /> + <UITableBody + :data="sortedUsers" + maxHeight="47rem" + :active="currentUser" + v-slot="{ item: user }" + > + <div class="py-2 col-1" @click="selectUser(user.user)"> + <font-awesome-icon + v-tooltip="roleLabel(user.role)" + :icon="roleIcon(user.role)" + class="fa-lg" + ></font-awesome-icon> + </div> + <div class="py-2 col-3" @click="selectUser(user.user)"> + {{ user.user }} + </div> + <div class="py-2 col-2" @click="selectUser(user.user)"> + {{ user.country }} + </div> + <div class="py-2 col-3" @click="selectUser(user.user)"> + {{ user.email }} + </div> + <div class="py-2 col text-right"> + <button + @click="sendTestMail(user.user)" + class="btn btn-sm btn-dark mr-1" + v-tooltip="$gettext('Send testmail')" + v-if="user.email" + > + <font-awesome-icon icon="paper-plane"></font-awesome-icon> + </button> + <button + @click="deleteUser(user.user)" + class="btn btn-sm btn-dark" + v-tooltip="$gettext('Delete user')" + > + <font-awesome-icon icon="trash" /> + </button> + </div> + </UITableBody> <div class="d-flex mx-auto align-items-center"> <button @click="prevPage" @@ -110,70 +83,44 @@ </div> </template> -<style lang="scss" scoped> -.addbutton { - position: absolute; - bottom: $offset; - right: $offset; -} +<style lang="sass" scoped> +.addbutton + position: absolute + bottom: $offset + right: $offset -.content { - width: 100%; -} - -.userdetails { - width: 50%; -} +.content + width: 100% -.main { - height: 100%; -} - -.icon { - font-size: large; -} +.userdetails + width: 50% -.userlist { - min-width: 520px; - height: 100%; -} +.main + height: 100% -.userlistsmall { - width: 100%; -} +.icon + font-size: large -.userlistextended { - width: 100%; -} - -.table { - margin: auto; -} +.userlist + min-width: 520px + height: 100% -.table th { - cursor: pointer; -} +.userlistsmall + width: 100% -.table th:first-child { - width: 50px; -} +.userlistextended + width: 100% -.table th, -td { - font-size: $smaller; - border-top: 0px !important; - text-align: left; - padding: $small-offset !important; -} - -.table td { - font-size: $smaller; - cursor: pointer; -} - -tr span { - display: flex; -} +.table-body + .row + > div + transition: background-color 0.3s, color 0.3s + &.active + > div + background-color: $color-info + color: #fff + a + color: #fff !important </style> <script> @@ -205,11 +152,10 @@ name: "userview", data() { return { - sortCriterion: "user", - pageSize: 20, - currentPage: 1, - userToDelete: "", - showDeleteUserPrompt: false + sortColumn: "user", + sortDirection: "ASC", + pageSize: 15, + currentPage: 1 }; }, components: { @@ -217,29 +163,30 @@ Spacer: () => import("@/components/Spacer") }, computed: { - ...mapGetters("usermanagement", ["isUserDetailsVisible"]), + ...mapGetters("usermanagement", [ + "isUserDetailsVisible", + "users", + "currentUser" + ]), ...mapState("application", ["showSidebar"]), - users() { - let users = [...this.$store.getters["usermanagement/users"]]; - users.sort((a, b) => { - if ( - a[this.sortCriterion].toLowerCase() < - b[this.sortCriterion].toLowerCase() - ) - return -1; - if ( - a[this.sortCriterion].toLowerCase() > - b[this.sortCriterion].toLowerCase() - ) - return 1; - return 0; - }); + sortedUsers() { const start = (this.currentPage - 1) * this.pageSize; - return users.slice(start, start + this.pageSize); + return this.users + .sort((a, b) => { + if ( + a[this.sortColumn].toLowerCase() < b[this.sortColumn].toLowerCase() + ) + return this.sortDirection === "ASC" ? -1 : 1; + if ( + a[this.sortColumn].toLowerCase() > b[this.sortColumn].toLowerCase() + ) + return this.sortDirection === "ASC" ? 1 : -1; + return 0; + }) + .slice(start, start + this.pageSize); }, pages() { - let users = [...this.$store.getters["usermanagement/users"]]; - return Math.ceil(users.length / this.pageSize); + return Math.ceil(this.users.length / this.pageSize); }, tableStyle() { return { @@ -282,29 +229,19 @@ }); }); }, - tween() {}, nextPage() { if (this.currentPage < this.pages) { - document.querySelector("#datatable").classList.add("fadeOut"); - setTimeout(() => { - document.querySelector("#datatable").classList.remove("fadeOut"); - this.currentPage += 1; - }, 10); + this.currentPage += 1; } - return; }, prevPage() { if (this.currentPage > 0) { - document.querySelector("#datatable").classList.add("fadeOut"); - setTimeout(() => { - document.querySelector("#datatable").classList.remove("fadeOut"); - this.currentPage -= 1; - }, 10); + this.currentPage -= 1; } - return; }, - sortBy(criterion) { - this.sortCriterion = criterion; + sortBy(sorting) { + this.sortColumn = sorting.sortColumn; + this.sortDirection = sorting.sortDirection; }, deleteUser(name) { this.$store.commit("application/popup", {
--- a/client/src/main.js Mon Mar 04 08:32:05 2019 +0100 +++ b/client/src/main.js Mon Mar 04 16:01:20 2019 +0100 @@ -38,7 +38,7 @@ import "../node_modules/animate.css/animate.min.css"; import "../node_modules/ol/ol.css"; import "../node_modules/highlight.js/styles/paraiso-dark.css"; -import "../node_modules/vue-snotify/styles/material.css"; +import "../node_modules/vue-snotify/styles/simple.css"; // fontawesome5 icons import {