mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-03-03 16:05:56 +00:00
Webapp: add basic multiple admin users
This commit is contained in:
parent
1e7ec18a2f
commit
5a16af40fd
9 changed files with 522 additions and 2 deletions
94
src/components/settings/Users/AddUser.vue
Normal file
94
src/components/settings/Users/AddUser.vue
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<h4 class="mt-4">
|
||||||
|
{{ $t("Create an admin account") }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<form data-cy="setup-form" @submit.prevent="create">
|
||||||
|
<div class="my-3">
|
||||||
|
<label class="form-label d-block">
|
||||||
|
{{ $t("Username") }}
|
||||||
|
<input
|
||||||
|
v-model="username"
|
||||||
|
type="text"
|
||||||
|
class="form-control mt-2"
|
||||||
|
:placeholder="$t('Username')"
|
||||||
|
required
|
||||||
|
:disabled="creating"
|
||||||
|
data-cy="username-input"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label d-block">
|
||||||
|
{{ $t("Password") }}
|
||||||
|
<input
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
class="form-control mt-2"
|
||||||
|
:placeholder="$t('Password')"
|
||||||
|
required
|
||||||
|
:disabled="creating"
|
||||||
|
data-cy="password-input"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label d-block">
|
||||||
|
{{ $t("Repeat Password") }}
|
||||||
|
<input
|
||||||
|
v-model="repeatPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control mt-2"
|
||||||
|
:placeholder="$t('Repeat Password')"
|
||||||
|
required
|
||||||
|
:disabled="creating"
|
||||||
|
data-cy="password-repeat-input"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" type="submit" :disabled="creating" data-cy="submit-create-admin-form">
|
||||||
|
<span v-show="creating" class="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
|
||||||
|
{{ $t("Create") }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
creating: false,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
repeatPassword: "",
|
||||||
|
}),
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Create an admin account
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
create() {
|
||||||
|
this.creating = true;
|
||||||
|
|
||||||
|
if (this.password !== this.repeatPassword) {
|
||||||
|
toast.error(this.$t("PasswordsDoNotMatch"));
|
||||||
|
this.creating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("setup", this.username, this.password, (res) => {
|
||||||
|
this.creating = false;
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
|
||||||
|
res.ok && this.$router.push({ name: "settings.users" });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
205
src/components/settings/Users/EditUser.vue
Normal file
205
src/components/settings/Users/EditUser.vue
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="loading" class="d-flex align-items-center justify-content-center my-3 spinner">
|
||||||
|
<font-awesome-icon icon="spinner" size="2x" spin />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<h5 class="my-4 settings-subheading">{{ $t("Identity") }}</h5>
|
||||||
|
<form @submit.prevent="save({ username })">
|
||||||
|
<label class="form-label d-block mb-3">
|
||||||
|
{{ $t("Username") }}
|
||||||
|
<input
|
||||||
|
v-model="username"
|
||||||
|
:placeholder="$t('Username')"
|
||||||
|
class="form-control mt-2"
|
||||||
|
required
|
||||||
|
:disabled="saving"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" type="submit" :disabled="saving">
|
||||||
|
<span v-show="saving" class="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
|
||||||
|
{{ $t("Update Username") }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h5 class="mt-5 mb-4 settings-subheading">{{ $t("Change Password") }}</h5>
|
||||||
|
<form class="mb-3" @submit.prevent="savePassword">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label d-block">
|
||||||
|
{{ $t("Current Password") }}
|
||||||
|
<input
|
||||||
|
v-model="currentPassword"
|
||||||
|
type="password"
|
||||||
|
:placeholder="$t('Current Password')"
|
||||||
|
class="form-control mt-2"
|
||||||
|
required
|
||||||
|
:disabled="savingPassword"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label d-block">
|
||||||
|
{{ $t("New Password") }}
|
||||||
|
<input
|
||||||
|
v-model="newPassword"
|
||||||
|
type="password"
|
||||||
|
:placeholder="$t('New Password')"
|
||||||
|
class="form-control mt-2"
|
||||||
|
required
|
||||||
|
:disabled="savingPassword"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label d-block">
|
||||||
|
{{ $t("Repeat New Password") }}
|
||||||
|
<input
|
||||||
|
v-model="repeatNewPassword"
|
||||||
|
type="password"
|
||||||
|
:placeholder="$t('Repeat New Password')"
|
||||||
|
class="form-control mt-2"
|
||||||
|
required
|
||||||
|
:disabled="savingPassword"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" type="submit" :disabled="savingPassword">
|
||||||
|
<span v-show="savingPassword" class="spinner-border spinner-border-sm" role="status" aria-hidden="true" />
|
||||||
|
{{ $t("Update Password") }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h5 class="mt-5 mb-4 settings-subheading">{{ $t("Permissions") }}</h5>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input
|
||||||
|
:checked="active"
|
||||||
|
class="form-check-input"
|
||||||
|
style="scale: 1.4; cursor: pointer;"
|
||||||
|
type="checkbox"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="debounceCheckboxClick(() => { active = !active; save({ active }); })"
|
||||||
|
>
|
||||||
|
<div class="ps-2">{{ $t("Active") }}</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Debounce } from "../../../util-frontend.js";
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
loading: false,
|
||||||
|
username: "",
|
||||||
|
saving: false,
|
||||||
|
currentPassword: "",
|
||||||
|
newPassword: "",
|
||||||
|
repeatNewPassword: "",
|
||||||
|
savingPassword: false,
|
||||||
|
active: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.getUser();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// Used to ignore one of the two "click" events fired when clicking on the checkbox label
|
||||||
|
debounceCheckboxClick: new Debounce(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user from server
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
getUser() {
|
||||||
|
this.loading = true;
|
||||||
|
this.$root.getSocket().emit("getUser", this.id, (res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.ok) {
|
||||||
|
const { username, active } = res.user;
|
||||||
|
|
||||||
|
this.username = username;
|
||||||
|
this.active = active;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check new passwords match before saving it
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
savePassword() {
|
||||||
|
this.savingPassword = true;
|
||||||
|
const { currentPassword, newPassword, repeatNewPassword } = this;
|
||||||
|
|
||||||
|
if (newPassword !== repeatNewPassword) {
|
||||||
|
toast.error(this.$t("PasswordsDoNotMatch"));
|
||||||
|
this.savingPassword = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$root
|
||||||
|
.getSocket()
|
||||||
|
.emit(
|
||||||
|
"changePassword",
|
||||||
|
this.id,
|
||||||
|
{
|
||||||
|
currentPassword,
|
||||||
|
newPassword
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
this.savingPassword = false;
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.currentPassword = "";
|
||||||
|
this.newPassword = "";
|
||||||
|
this.repeatNewPassword = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save user changes
|
||||||
|
* @param {object} user user to save
|
||||||
|
* @param {string} [user.username] username used as login identifier.
|
||||||
|
* @param {boolean} [user.active] is the user authorized to login?
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
save(user) {
|
||||||
|
this.saving = true;
|
||||||
|
|
||||||
|
this.$root
|
||||||
|
.getSocket()
|
||||||
|
.emit(
|
||||||
|
"saveUser",
|
||||||
|
{
|
||||||
|
id: this.id,
|
||||||
|
...user
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
this.saving = false;
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
153
src/components/settings/Users/Users.vue
Normal file
153
src/components/settings/Users/Users.vue
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
<template>
|
||||||
|
<div class="my-4">
|
||||||
|
<div class="mx-0 mx-lg-4 pt-1 mb-4">
|
||||||
|
<button class="btn btn-primary" @click="$router.push({ name: 'settings.users.add' })">
|
||||||
|
<font-awesome-icon icon="plus" /> {{ $t("Add New User") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="d-flex align-items-center justify-content-center my-3 spinner">
|
||||||
|
<font-awesome-icon icon="spinner" size="2x" spin />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="my-3">
|
||||||
|
<RouterLink
|
||||||
|
v-for="({ id, username, active }, index) in usersList"
|
||||||
|
:key="id"
|
||||||
|
class="d-flex align-items-center mx-0 mx-lg-4 py-1 text-decoration-none users-list-row"
|
||||||
|
:to="{ name: 'settings.users.edit', params: { id } }"
|
||||||
|
>
|
||||||
|
<div class="col-10 col-sm-5 m-2 flex-shrink-1 fw-bold">
|
||||||
|
{{ username }}
|
||||||
|
</div>
|
||||||
|
<div class="col-5 px-1 flex-shrink-1 d-none d-sm-flex gap-2 align-items-center">
|
||||||
|
<font-awesome-icon :class="active ? 'text-success' : 'text-muted'" :icon="active ? 'check-circle' : 'times-circle'" />
|
||||||
|
<div>{{ $t(active ? "Active" : "Inactive") }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-2 pe-2 pe-lg-3 d-flex justify-content-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-ban-user btn ms-2 py-1"
|
||||||
|
:class="active ? 'btn-outline-danger' : 'btn-outline-success'"
|
||||||
|
:disabled="processing"
|
||||||
|
@click.prevent="active ? disableConfirm(usersList[index]) : toggleActiveUser(usersList[index])"
|
||||||
|
>
|
||||||
|
<font-awesome-icon class="" :icon="active ? 'user-slash' : 'user-check'" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Confirm
|
||||||
|
ref="confirmDisable"
|
||||||
|
btn-style="btn-danger"
|
||||||
|
:yes-text="$t('Yes')"
|
||||||
|
:no-text="$t('No')"
|
||||||
|
@yes="toggleActiveUser(disablingUser)"
|
||||||
|
@no="disablingUser = null"
|
||||||
|
>
|
||||||
|
{{ $t("confirmDisableUserMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useToast } from "vue-toastification";
|
||||||
|
import Confirm from "../../Confirm.vue";
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { Confirm },
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
loading: false,
|
||||||
|
processing: false,
|
||||||
|
usersList: null,
|
||||||
|
disablingUser: null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.getUsers();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* Get list of users from server
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
getUsers() {
|
||||||
|
this.loading = true;
|
||||||
|
this.$root.getSocket().emit("getUsers", (res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.ok) {
|
||||||
|
this.usersList = res.users;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show confirmation for disabling a user
|
||||||
|
* @param {object} user the user to confirm disable in the local usersList
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
disableConfirm(user) {
|
||||||
|
this.disablingUser = user;
|
||||||
|
this.$refs.confirmDisable.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable a user from server
|
||||||
|
* @param {object} user the user to disable in the local usersList
|
||||||
|
* @param {boolean} user.active is the user authorized to login?
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
toggleActiveUser({ active, ...rest }) {
|
||||||
|
this.processing = true;
|
||||||
|
this.$root.getSocket().emit(
|
||||||
|
"saveUser",
|
||||||
|
{
|
||||||
|
...rest,
|
||||||
|
active: !active
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
this.processing = false;
|
||||||
|
this.disablingUser &&= null;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.getUsers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../../../assets/vars.scss";
|
||||||
|
|
||||||
|
.btn-ban-user {
|
||||||
|
padding-left: 7px;
|
||||||
|
padding-right: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.users-list-row {
|
||||||
|
cursor: pointer;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.125);
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
border-top: 1px solid $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $highlight-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark &:hover {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
29
src/components/settings/Users/routes.js
Normal file
29
src/components/settings/Users/routes.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { h } from "vue";
|
||||||
|
import { RouterView } from "vue-router";
|
||||||
|
|
||||||
|
// Needed for settings enter/leave CSS animation
|
||||||
|
const AnimatedRouterView = () => h("div", [ h(RouterView) ]);
|
||||||
|
AnimatedRouterView.displayName = "AnimatedRouterView";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
path: "users",
|
||||||
|
component: AnimatedRouterView,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
name: "settings.users",
|
||||||
|
component: () => import("./Users.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "add",
|
||||||
|
name: "settings.users.add",
|
||||||
|
component: () => import("./AddUser.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "edit/:id",
|
||||||
|
name: "settings.users.edit",
|
||||||
|
props: true,
|
||||||
|
component: () => import("./EditUser.vue")
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
|
@ -50,6 +50,8 @@ import {
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faClone,
|
faClone,
|
||||||
faCertificate,
|
faCertificate,
|
||||||
|
faUserSlash,
|
||||||
|
faUserCheck,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -97,6 +99,8 @@ library.add(
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faClone,
|
faClone,
|
||||||
faCertificate,
|
faCertificate,
|
||||||
|
faUserSlash,
|
||||||
|
faUserCheck,
|
||||||
);
|
);
|
||||||
|
|
||||||
export { FontAwesomeIcon };
|
export { FontAwesomeIcon };
|
||||||
|
|
|
@ -1051,5 +1051,12 @@
|
||||||
"From":"From",
|
"From":"From",
|
||||||
"Can be found on:": "Can be found on: {0}",
|
"Can be found on:": "Can be found on: {0}",
|
||||||
"The phone number of the recipient in E.164 format.": "The phone number of the recipient in E.164 format.",
|
"The phone number of the recipient in E.164 format.": "The phone number of the recipient in E.164 format.",
|
||||||
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.":"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies."
|
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.": "Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.",
|
||||||
|
"Users": "Users",
|
||||||
|
"Add New User": "Add New User",
|
||||||
|
"confirmDisableUserMsg": "Are you sure you want to disable this user? The user will not be able to login anymore.",
|
||||||
|
"Create an admin account": "Create an admin account",
|
||||||
|
"Identity": "Identity",
|
||||||
|
"Update Username": "Update Username",
|
||||||
|
"Permissions": "Permissions"
|
||||||
}
|
}
|
|
@ -130,6 +130,13 @@ export default {
|
||||||
security: {
|
security: {
|
||||||
title: this.$t("Security"),
|
title: this.$t("Security"),
|
||||||
},
|
},
|
||||||
|
users: {
|
||||||
|
title: this.$t("Users"),
|
||||||
|
children: {
|
||||||
|
add: { title: this.$t("Add") },
|
||||||
|
edit: { title: this.$t("Edit") }
|
||||||
|
},
|
||||||
|
},
|
||||||
"api-keys": {
|
"api-keys": {
|
||||||
title: this.$t("API Keys")
|
title: this.$t("API Keys")
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,6 +27,7 @@ import General from "./components/settings/General.vue";
|
||||||
const Notifications = () => import("./components/settings/Notifications.vue");
|
const Notifications = () => import("./components/settings/Notifications.vue");
|
||||||
import ReverseProxy from "./components/settings/ReverseProxy.vue";
|
import ReverseProxy from "./components/settings/ReverseProxy.vue";
|
||||||
import Tags from "./components/settings/Tags.vue";
|
import Tags from "./components/settings/Tags.vue";
|
||||||
|
import usersSettingsRoutes from "./components/settings/Users/routes.js";
|
||||||
import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
||||||
const Security = () => import("./components/settings/Security.vue");
|
const Security = () => import("./components/settings/Security.vue");
|
||||||
import Proxies from "./components/settings/Proxies.vue";
|
import Proxies from "./components/settings/Proxies.vue";
|
||||||
|
@ -124,6 +125,7 @@ const routes = [
|
||||||
path: "security",
|
path: "security",
|
||||||
component: Security,
|
component: Security,
|
||||||
},
|
},
|
||||||
|
usersSettingsRoutes,
|
||||||
{
|
{
|
||||||
path: "api-keys",
|
path: "api-keys",
|
||||||
component: APIKeys,
|
component: APIKeys,
|
||||||
|
|
|
@ -213,3 +213,22 @@ export function getToastErrorTimeout() {
|
||||||
|
|
||||||
return errorTimeout;
|
return errorTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get debounced function
|
||||||
|
* @returns {Function} debounced function
|
||||||
|
*/
|
||||||
|
export function Debounce() {
|
||||||
|
let timeout = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* exec callback function after delay if no new call to function happens
|
||||||
|
* @param {Function} callback function to execute after delay
|
||||||
|
* @param {number} [delay=100] delay before execute the callback if no new call to function happens
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
return function (callback, delay = 100) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => callback(), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue