Compare commits

...

10 Commits
0.1.6 ... 0.2.0

Author SHA1 Message Date
77690702c0 🚀RELEASE v0.2.0
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-11 21:17:35 +01:00
e0093480d9 Merge branch 'feature/12-user-management' into dev 2021-01-11 21:17:12 +01:00
c7679b7a67 [tmp] - disable darkmode + re-enable sw 2021-01-11 21:17:05 +01:00
e6ac34bde8 🐞 [tmp] - nginx.conf - disable .js file caching
Some checks reported errors
continuous-integration/drone/push Build was killed
2021-01-11 21:10:40 +01:00
8c4b595c30 Merge branch 'dev' into feature/12-user-management 2021-01-11 21:08:44 +01:00
be629e5c6b 🕕 set manual refresh time to 2min
Some checks reported errors
continuous-integration/drone/push Build was killed
2021-01-11 21:08:39 +01:00
63569684a3 ℹ update jwtinfo store on token refresh 2021-01-11 21:08:14 +01:00
5937a0d7ce 🔒 added rendering based on permission level
ref #12
2021-01-11 21:07:30 +01:00
4512272c1c UserDetail - delete
ref #12
2021-01-11 21:06:47 +01:00
ce1f3842e0 🖊 UserDetail - reactivity on edit + update functionality
ref #12
2021-01-11 20:41:57 +01:00
12 changed files with 270 additions and 207 deletions

View File

@ -2,10 +2,23 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [0.2.0](https://git.odit.services/lfk/frontend/compare/0.1.6...0.2.0)
- 🔒 added rendering based on permission level [`5937a0d`](https://git.odit.services/lfk/frontend/commit/5937a0d7ce970d38ddb0e4c6a02d11f8c8b53e9b)
- ❌ UserDetail - delete [`4512272`](https://git.odit.services/lfk/frontend/commit/4512272c1c2a5c861d8ac4c9908244df125dc3d9)
- 🖊 UserDetail - reactivity on edit + update functionality [`ce1f384`](https://git.odit.services/lfk/frontend/commit/ce1f3842e0f581d9e94ea6079cf223d945f7465d)
- [tmp] - disable darkmode + re-enable sw [`c7679b7`](https://git.odit.services/lfk/frontend/commit/c7679b7a67b362a7fbf796f612892bdfac50731c)
- 🕕 set manual refresh time to 2min [`be629e5`](https://git.odit.services/lfk/frontend/commit/be629e5c6b076bf9a28dcc953e97478d244d8413)
- 🐞 [tmp] - nginx.conf - disable .js file caching [`e6ac34b`](https://git.odit.services/lfk/frontend/commit/e6ac34bde8d288a45e83fc6723a2bbe952f2622a)
- update jwtinfo store on token refresh [`6356968`](https://git.odit.services/lfk/frontend/commit/63569684a392bf0c24c9c2efd7945114d1a230a5)
#### [0.1.6](https://git.odit.services/lfk/frontend/compare/0.1.5...0.1.6)
> 10 January 2021
- ✨ UsersOverview - user delete [`f0c100a`](https://git.odit.services/lfk/frontend/commit/f0c100aee47d6a5aeb0995db79b268f33bf316e9)
- 🔒 UserDetail - added basic layout for permission change [`81c1537`](https://git.odit.services/lfk/frontend/commit/81c1537bada2c87127385d9110d48459cd1b505f)
- 🚀RELEASE v0.1.6 [`ee01c3a`](https://git.odit.services/lfk/frontend/commit/ee01c3a059c435add68938657955cbd2c5851c50)
- 📧 UserDetail - email input [`f856c6a`](https://git.odit.services/lfk/frontend/commit/f856c6ae3792c93aee148339d89e3978db6e9293)
- ✨ UserDetail multiselect layout for groups [`98ecfab`](https://git.odit.services/lfk/frontend/commit/98ecfab0325e35c5418775f7162049b56e5f332f)
- UserDetail - placeholder for permission picker 🔒 [`b948b8c`](https://git.odit.services/lfk/frontend/commit/b948b8c1a48e5b4bc6a083139d26100ef4499970)

View File

@ -9,7 +9,7 @@ http {
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
location ~* \.(?:ico|css|gif|jpe?g|png)$ {
expires 1y;
add_header Pragma public;
add_header Cache-Control "public";

View File

@ -1,6 +1,6 @@
{
"name": "@odit/lfk-frontend",
"version": "0.1.6",
"version": "0.2.0",
"scripts": {
"i18n-order": "node order.js",
"dev": "snowpack dev",

View File

@ -13,7 +13,7 @@
</head>
<body>
<span style="display: none;visibility: hidden;" id="buildinfo">RELEASE_INFO-0.1.6-RELEASE_INFO</span>
<span style="display: none;visibility: hidden;" id="buildinfo">RELEASE_INFO-0.2.0-RELEASE_INFO</span>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script src="/env.js"></script>
<script defer type="module" src="/_dist_/index.js"></script>

View File

@ -49,7 +49,7 @@
OpenAPI.BASE = config.baseurl;
import { register as registerSW } from "./swmodule";
store.init();
// registerSW();
registerSW();
</script>
<Route>

View File

@ -2,7 +2,7 @@
import { _ } from "svelte-i18n";
import { clickOutside } from "./outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { tracks as tracksstore } from "../store.js";
import { tracks as usersstore } from "../store.js";
import { UserService } from "@odit/lfk-client-js";
import isEmail from "validator/es/lib/isEmail";
import Toastify from "toastify-js";
@ -68,11 +68,11 @@
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
let storeval = [];
tracksstore.subscribe((val) => {
usersstore.subscribe((val) => {
storeval = val;
});
storeval.push(result);
tracksstore.set(storeval);
usersstore.set(storeval);
})
.catch((err) => {
//

View File

@ -1,9 +1,12 @@
<script>
import { _ } from "svelte-i18n";
const releaseinfo = document
.getElementById("buildinfo")
.textContent.replace("RELEASE_INFO-", "")
.replace("-RELEASE_INFO", "");
$: releaseinfo = "";
setTimeout(() => {
releaseinfo = document
.getElementById("buildinfo")
.textContent.replace("RELEASE_INFO-", "")
.replace("-RELEASE_INFO", "");
}, 1500);
const year = new Date().getFullYear();
</script>

View File

@ -1,28 +1,50 @@
<script>
import { _ } from "svelte-i18n";
import lodashIsEqual from "lodash.isequal";
import store from "../store";
import { UserService } from "@odit/lfk-client-js";
import "gridjs/dist/theme/mermaid.css";
import PromiseError from "./PromiseError.svelte";
export let params;
const user_promise = UserService.userControllerGetOne(params.userid);
let data_loaded = false;
let original_data = undefined;
$: delete_triggered = false;
$: original_data = {};
$: editable_userdata = undefined;
user_promise.then((data) => {
data_loaded = true;
original_data = data;
original_data = Object.assign(original_data, data);
editable_userdata = data;
});
// $: changes_performed = lodashIsEqual(original_data, editable_userdata);
$: changes_performed = !lodashIsEqual({ test: 1 }, { test: 1 });
$: changes_performed = !lodashIsEqual(original_data, editable_userdata);
function submit() {
if (data_loaded === true && changes_performed === true) {
console.log("ok, submitting...");
console.log(editable_userdata);
UserService.userControllerPut(original_data.id, editable_userdata)
.then((resp) => {
console.log(resp);
Object.assign(original_data, editable_userdata);
original_data = editable_userdata;
Object.assign(original_data, editable_userdata);
})
.catch((err) => {
console.log(err);
});
} else {
console.log("no changes performed");
}
}
function deleteUser() {
UserService.userControllerRemove(original_data.id, true)
.then((resp) => {
console.log(resp);
location.replace("./");
})
.catch((err) => {
console.log(err);
});
}
</script>
{#await user_promise}
@ -63,36 +85,47 @@
<polyline points="12 5 19 12 12 19" /></svg>
</li>
<li class="flex items-center">
<span class="mr-2">{user.firstname}
{user.middlename || ''}
{user.lastname}</span>
<span class="mr-2">{original_data.firstname}
{original_data.middlename || ''}
{original_data.lastname}</span>
</li>
</ol>
</nav>
</div>
</div>
<div class="mb-8 text-3xl font-extrabold leading-tight">
{user.firstname}
{user.middlename || ''}
{user.lastname}
<span data-id="user_actions_${user.id}">
<button
class="hidden w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-400 text-base font-medium text-white sm:w-auto sm:text-sm"
data-userid="${user.id}"
onclick="user__delete_cancel()">{$_('cancel')}</button>
<button
class="hidden w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-500 text-base font-medium text-white sm:w-auto sm:text-sm"
data-userid="${user.id}"
onclick="user__delete_confirm()">{$_('confirm-delete')}</button>
<button
type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('delete-user')}</button>
<button
disabled={!changes_performed}
class:opacity-50={!changes_performed}
type="button"
on:click={submit}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('save-changes')}</button>
{original_data.firstname}
{original_data.middlename || ''}
{original_data.lastname}
<span data-id="user_actions_${editable_userdata.id}">
{#if store.state.jwtinfo.userdetails.permissions.includes('USER:DELETE')}
{#if delete_triggered}
<button
on:click={deleteUser}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('confirm-delete')}</button>
<button
on:click={() => {
delete_triggered = !delete_triggered;
}}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-400 text-base font-medium text-white sm:w-auto sm:text-sm">{$_('cancel')}</button>
{/if}
{#if !delete_triggered}
<button
on:click={() => {
delete_triggered = true;
}}
type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('delete-user')}</button>
{/if}
{/if}
{#if !delete_triggered}
<button
disabled={!changes_performed}
class:opacity-50={!changes_performed}
type="button"
on:click={submit}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('save-changes')}</button>
{/if}
</span>
</div>
@ -101,7 +134,7 @@
<img
alt={$_('profile-picture')}
class="inline-block h-20 w-20 rounded-full overflow-hidden bg-gray-100"
src={user.profilePic} />
src={editable_userdata.profilePic} />
<!-- <span
class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100"><svg
class="h-full w-full text-gray-300"

View File

@ -1,23 +1,30 @@
<script>
import { _ } from "svelte-i18n";
import store from "../store";
import AddUserModal from "./AddUserModal.svelte";
export let modal_open = false;
import UsersOverview from "./UsersOverview.svelte";
console.log(store.state.jwtinfo.userdetails.permissions);
</script>
<section class="container p-5">
<span class="mb-1 text-3xl font-extrabold leading-tight">
{$_('users')}
<button
on:click={() => {
modal_open = true;
}}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
{$_('create-user')}
</button>
{#if store.state.jwtinfo.userdetails.permissions.includes('USER:CREATE')}
<button
on:click={() => {
modal_open = true;
}}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
{$_('create-user')}
</button>
{/if}
</span>
<p class="mb-8 text-lg text-gray-500">{$_('manage-admin-users')}</p>
<UsersOverview />
</section>
<AddUserModal bind:modal_open />
{#if store.state.jwtinfo.userdetails.permissions.includes('USER:CREATE')}
<AddUserModal bind:modal_open />
{/if}

View File

@ -5,6 +5,7 @@
const users_promise = UserService.userControllerGetAll();
import "gridjs/dist/theme/mermaid.css";
import { users as usersstore } from "../store.js";
import store from "../store";
import UsersEmptyState from "./UsersEmptyState.svelte";
$: searchvalue = "";
$: active_deletes = [];
@ -19,163 +20,167 @@
});
</script>
{#await users_promise}
<div
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
role="alert">
<p class="font-bold">users are being loaded...</p>
<p class="text-sm">{$_('this-might-take-a-moment')}</p>
</div>
{:then users}
{#if userscache.length === 0}
<UsersEmptyState />
{:else}
{#if advanced_search}
advanced search
{:else}
<input
type="search"
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
{/if}
<button
on:click={() => {
advanced_search = !advanced_search;
}}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-600 text-base font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:ml-3 sm:w-auto sm:text-sm">
{#if advanced_search}
toggle simple search
{:else}toggle advanced search{/if}
</button>
{#if store.state.jwtinfo.userdetails.permissions.includes('USER:GET')}
{#await users_promise}
<div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
<table class="divide-y divide-gray-200 w-full">
<thead class="bg-gray-50">
<tr>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Groups
</th>
<th scope="col" class="relative px-6 py-3">
<span class="sr-only">Action</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each users as u}
{#if Object.values(u)
.toString()
.toLowerCase()
.includes(searchvalue)}
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
{#if u.profilePic}
<div class="flex-shrink-0 h-10 w-10">
<img
class="h-10 w-10 rounded-full"
src={u.profilePic}
alt="" />
</div>
{/if}
<div class="ml-4">
<div
class="text-sm font-medium text-gray-900 dark:text-gray-100">
{u.firstname}
{u.middlename || ''}
{u.lastname}
</div>
<div class="text-sm text-gray-500">
{u.email || u.username}
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
role="alert">
<p class="font-bold">users are being loaded...</p>
<p class="text-sm">{$_('this-might-take-a-moment')}</p>
</div>
{:then users}
{#if userscache.length === 0}
<UsersEmptyState />
{:else}
{#if advanced_search}
advanced search
{:else}
<input
type="search"
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
{/if}
<button
on:click={() => {
advanced_search = !advanced_search;
}}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-600 text-base font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:ml-3 sm:w-auto sm:text-sm">
{#if advanced_search}
toggle simple search
{:else}toggle advanced search{/if}
</button>
<div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
<table class="divide-y divide-gray-200 w-full">
<thead class="bg-gray-50">
<tr>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Groups
</th>
<th scope="col" class="relative px-6 py-3">
<span class="sr-only">Action</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each users as u}
{#if Object.values(u)
.toString()
.toLowerCase()
.includes(searchvalue)}
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
{#if u.profilePic}
<div class="flex-shrink-0 h-10 w-10">
<img
class="h-10 w-10 rounded-full"
src={u.profilePic}
alt="" />
</div>
{/if}
<div class="ml-4">
<div
class="text-sm font-medium text-gray-900 dark:text-gray-100">
{u.firstname}
{u.middlename || ''}
{u.lastname}
</div>
<div class="text-sm text-gray-500">
{u.email || u.username}
</div>
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{#if u.enabled}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Active</span>
{:else}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Inactive</span>
{/if}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{#each u.groups as g}
<a
href="../groups/{g.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">{g.name}</a>
{/each}
</td>
{#if active_deletes[u.id] === true}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
on:click={() => {
active_deletes[u.id] = false;
}}
tabindex="0"
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer">Cancel
Delete</button>
<button
on:click={() => {
UserService.userControllerRemove(u.id, true)
.then((resp) => {
console.log(resp);
// user deleted
users_promise.then((data) => {
console.log(data);
usersstore.set(data);
</td>
<td class="px-6 py-4 whitespace-nowrap">
{#if u.enabled}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Active</span>
{:else}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Inactive</span>
{/if}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{#each u.groups as g}
<a
href="../groups/{g.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">{g.name}</a>
{/each}
</td>
{#if active_deletes[u.id] === true}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
on:click={() => {
active_deletes[u.id] = false;
}}
tabindex="0"
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer">Cancel
Delete</button>
<button
on:click={() => {
UserService.userControllerRemove(u.id, true)
.then((resp) => {
console.log(resp);
// user deleted
users_promise.then((data) => {
console.log(data);
usersstore.set(data);
});
})
.catch((err) => {
// error deleting user
});
})
.catch((err) => {
// error deleting user
});
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">Confirm
Delete</button>
</td>
{:else}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a
href="./{u.id}"
class="text-indigo-600 hover:text-indigo-900">Edit</a>
<button
on:click={() => {
active_deletes[u.id] = true;
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">Delete</button>
</td>
{/if}
</tr>
{/if}
{/each}
</tbody>
</table>
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">Confirm
Delete</button>
</td>
{:else}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a
href="./{u.id}"
class="text-indigo-600 hover:text-indigo-900">Edit</a>
{#if store.state.jwtinfo.userdetails.permissions.includes('USER:DELETE')}
<button
on:click={() => {
active_deletes[u.id] = true;
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">Delete</button>
{/if}
</td>
{/if}
</tr>
{/if}
{/each}
</tbody>
</table>
</div>
{/if}
{:catch error}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_('general_promise_error')}</b>
{error}
</span>
</div>
{/if}
{:catch error}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_('general_promise_error')}</b>
{error}
</span>
</div>
{/await}
{/await}
{/if}

View File

@ -27,6 +27,8 @@ const store = () => {
AuthService.authControllerRefresh({ token: state.auth.refresh_token }).then((auth) => {
console.log('got new auth');
OpenAPI.TOKEN = auth.access_token;
const jwtinfo = JSON.parse(atob(auth.access_token.split('.')[1]));
state.jwtinfo = jwtinfo;
localForage.setItem('logindata', auth);
});
},
@ -39,8 +41,8 @@ const store = () => {
//
state.refreshInterval = setInterval(() => {
this.refreshAuth();
// 4min
}, 4 * 60000);
// 2min
}, 2 * 60000);
//
return state;
});

View File

@ -2,7 +2,7 @@ module.exports = {
purge: {
content: [ './src/**/*.svelte' ]
},
darkMode: 'media',
// darkMode: 'media',
variants: {},
plugins: [],
theme: {