Merge pull request #10542 from Budibase/user-limit-ui
Adds account locking if user limit is exceeded
This commit is contained in:
commit
4928778b0b
|
@ -62,6 +62,7 @@
|
||||||
"@budibase/frontend-core": "0.0.1",
|
"@budibase/frontend-core": "0.0.1",
|
||||||
"@budibase/shared-core": "0.0.1",
|
"@budibase/shared-core": "0.0.1",
|
||||||
"@budibase/string-templates": "0.0.1",
|
"@budibase/string-templates": "0.0.1",
|
||||||
|
"@budibase/types": "0.0.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||||
|
|
||||||
|
let modal
|
||||||
|
|
||||||
|
export let onConfirm
|
||||||
|
|
||||||
|
export function show() {
|
||||||
|
modal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
modal.hide()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={modal} on:hide={modal}>
|
||||||
|
<ModalContent
|
||||||
|
title="Your account is currently de-activated"
|
||||||
|
size="S"
|
||||||
|
showCancelButton={true}
|
||||||
|
showCloseIcon={false}
|
||||||
|
confirmText={"View plans"}
|
||||||
|
{onConfirm}
|
||||||
|
>
|
||||||
|
<Body size="S"
|
||||||
|
>Due to the free plan user limit being exceeded, your account has been
|
||||||
|
de-activated. Upgrade your plan to re-activate your account.</Body
|
||||||
|
>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
|
@ -3,7 +3,6 @@ import { temporalStore } from "builderStore"
|
||||||
import { admin, auth, licensing } from "stores/portal"
|
import { admin, auth, licensing } from "stores/portal"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { BANNER_TYPES } from "@budibase/bbui"
|
import { BANNER_TYPES } from "@budibase/bbui"
|
||||||
import { capitalise } from "helpers"
|
|
||||||
|
|
||||||
const oneDayInSeconds = 86400
|
const oneDayInSeconds = 86400
|
||||||
|
|
||||||
|
@ -146,23 +145,19 @@ const buildUsersAboveLimitBanner = EXPIRY_KEY => {
|
||||||
const userLicensing = get(licensing)
|
const userLicensing = get(licensing)
|
||||||
return {
|
return {
|
||||||
key: EXPIRY_KEY,
|
key: EXPIRY_KEY,
|
||||||
type: BANNER_TYPES.WARNING,
|
type: BANNER_TYPES.NEGATIVE,
|
||||||
onChange: () => {
|
onChange: () => {
|
||||||
defaultCacheFn(EXPIRY_KEY)
|
defaultCacheFn(EXPIRY_KEY)
|
||||||
},
|
},
|
||||||
criteria: () => {
|
criteria: () => {
|
||||||
return userLicensing.warnUserLimit
|
return userLicensing.errUserLimit
|
||||||
},
|
},
|
||||||
message: `${capitalise(
|
message: "Your Budibase account is de-activated. Upgrade your plan",
|
||||||
userLicensing.license.plan.type
|
|
||||||
)} plan changes - Users will be limited to ${
|
|
||||||
userLicensing.userLimit
|
|
||||||
} users in ${userLicensing.userLimitDays}`,
|
|
||||||
...{
|
...{
|
||||||
extraButtonText: "Find out more",
|
extraButtonText: "View plans",
|
||||||
extraButtonAction: () => {
|
extraButtonAction: () => {
|
||||||
defaultCacheFn(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER)
|
defaultCacheFn(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER)
|
||||||
window.location.href = "/builder/portal/users/users"
|
window.location.href = "https://budibase.com/pricing/"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
showCloseButton: true,
|
showCloseButton: true,
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
|
|
||||||
|
export let lockedAction
|
||||||
|
|
||||||
const handleDefaultClick = () => {
|
const handleDefaultClick = () => {
|
||||||
if (window.innerWidth < 640) {
|
if (window.innerWidth < 640) {
|
||||||
goToOverview()
|
goToOverview()
|
||||||
|
@ -29,7 +31,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app-row" on:click={handleDefaultClick}>
|
<div class="app-row" on:click={lockedAction || handleDefaultClick}>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="app-icon">
|
<div class="app-icon">
|
||||||
<Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} />
|
<Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} />
|
||||||
|
@ -58,8 +60,11 @@
|
||||||
|
|
||||||
<div class="app-row-actions">
|
<div class="app-row-actions">
|
||||||
<AppLockModal {app} buttonSize="M" />
|
<AppLockModal {app} buttonSize="M" />
|
||||||
<Button size="S" secondary on:click={goToOverview}>Manage</Button>
|
<Button size="S" secondary on:click={lockedAction || goToOverview}
|
||||||
<Button size="S" primary on:click={goToBuilder}>Edit</Button>
|
>Manage</Button
|
||||||
|
>
|
||||||
|
<Button size="S" primary on:click={lockedAction || goToBuilder}>Edit</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -133,7 +133,7 @@
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#if $licensing.usageMetrics?.dayPasses >= 100}
|
{#if $licensing.usageMetrics?.dayPasses >= 100 || $licensing.errUserLimit}
|
||||||
<div>
|
<div>
|
||||||
<Layout gap="S" justifyItems="center">
|
<Layout gap="S" justifyItems="center">
|
||||||
<img class="spaceman" alt="spaceman" src={Spaceman} />
|
<img class="spaceman" alt="spaceman" src={Spaceman} />
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||||
|
import AccountLockedModal from "components/portal/licensing/AccountLockedModal.svelte"
|
||||||
|
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -28,6 +29,7 @@
|
||||||
let template
|
let template
|
||||||
let creationModal
|
let creationModal
|
||||||
let appLimitModal
|
let appLimitModal
|
||||||
|
let accountLockedModal
|
||||||
let creatingApp = false
|
let creatingApp = false
|
||||||
let searchTerm = ""
|
let searchTerm = ""
|
||||||
let creatingFromTemplate = false
|
let creatingFromTemplate = false
|
||||||
|
@ -48,6 +50,11 @@
|
||||||
: true)
|
: true)
|
||||||
)
|
)
|
||||||
$: automationErrors = getAutomationErrors(enrichedApps)
|
$: automationErrors = getAutomationErrors(enrichedApps)
|
||||||
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
|
|
||||||
|
const usersLimitLockAction = $licensing?.errUserLimit
|
||||||
|
? () => accountLockedModal.show()
|
||||||
|
: null
|
||||||
|
|
||||||
const enrichApps = (apps, user, sortBy) => {
|
const enrichApps = (apps, user, sortBy) => {
|
||||||
const enrichedApps = apps.map(app => ({
|
const enrichedApps = apps.map(app => ({
|
||||||
|
@ -189,6 +196,9 @@
|
||||||
creatingFromTemplate = true
|
creatingFromTemplate = true
|
||||||
createAppFromTemplateUrl(initInfo.init_template)
|
createAppFromTemplateUrl(initInfo.init_template)
|
||||||
}
|
}
|
||||||
|
if (usersLimitLockAction) {
|
||||||
|
usersLimitLockAction()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting init info")
|
notifications.error("Error getting init info")
|
||||||
}
|
}
|
||||||
|
@ -230,20 +240,30 @@
|
||||||
<Layout noPadding gap="L">
|
<Layout noPadding gap="L">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button size="M" cta on:click={initiateAppCreation}>
|
<Button
|
||||||
|
size="M"
|
||||||
|
cta
|
||||||
|
on:click={usersLimitLockAction || initiateAppCreation}
|
||||||
|
>
|
||||||
Create new app
|
Create new app
|
||||||
</Button>
|
</Button>
|
||||||
{#if $apps?.length > 0}
|
{#if $apps?.length > 0}
|
||||||
<Button
|
<Button
|
||||||
size="M"
|
size="M"
|
||||||
secondary
|
secondary
|
||||||
on:click={$goto("/builder/portal/apps/templates")}
|
on:click={usersLimitLockAction ||
|
||||||
|
$goto("/builder/portal/apps/templates")}
|
||||||
>
|
>
|
||||||
View templates
|
View templates
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !$apps?.length}
|
{#if !$apps?.length}
|
||||||
<Button size="L" quiet secondary on:click={initiateAppImport}>
|
<Button
|
||||||
|
size="L"
|
||||||
|
quiet
|
||||||
|
secondary
|
||||||
|
on:click={usersLimitLockAction || initiateAppImport}
|
||||||
|
>
|
||||||
Import app
|
Import app
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -267,7 +287,7 @@
|
||||||
|
|
||||||
<div class="app-table">
|
<div class="app-table">
|
||||||
{#each filteredApps as app (app.appId)}
|
{#each filteredApps as app (app.appId)}
|
||||||
<AppRow {app} />
|
<AppRow {app} lockedAction={usersLimitLockAction} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -294,6 +314,11 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<AppLimitModal bind:this={appLimitModal} />
|
<AppLimitModal bind:this={appLimitModal} />
|
||||||
|
<AccountLockedModal
|
||||||
|
bind:this={accountLockedModal}
|
||||||
|
onConfirm={() =>
|
||||||
|
isOwner ? $licensing.goToUpgradePage() : $licensing.goToPricingPage()}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.title {
|
.title {
|
||||||
|
|
|
@ -30,8 +30,8 @@
|
||||||
$: hasError = userData.find(x => x.error != null)
|
$: hasError = userData.find(x => x.error != null)
|
||||||
|
|
||||||
$: userCount = $licensing.userCount + userData.length
|
$: userCount = $licensing.userCount + userData.length
|
||||||
$: willReach = licensing.willReachUserLimit(userCount)
|
$: reached = licensing.usersLimitReached(userCount)
|
||||||
$: willExceed = licensing.willExceedUserLimit(userCount)
|
$: exceeded = licensing.usersLimitExceeded(userCount)
|
||||||
|
|
||||||
function removeInput(idx) {
|
function removeInput(idx) {
|
||||||
userData = userData.filter((e, i) => i !== idx)
|
userData = userData.filter((e, i) => i !== idx)
|
||||||
|
@ -87,7 +87,7 @@
|
||||||
confirmDisabled={disabled}
|
confirmDisabled={disabled}
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
disabled={hasError || !userData.length || willExceed}
|
disabled={hasError || !userData.length || exceeded}
|
||||||
>
|
>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Label>Email address</Label>
|
<Label>Email address</Label>
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if willReach}
|
{#if reached}
|
||||||
<div class="user-notification">
|
<div class="user-notification">
|
||||||
<Icon name="Info" />
|
<Icon name="Info" />
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -25,10 +25,10 @@
|
||||||
$: invalidEmails = []
|
$: invalidEmails = []
|
||||||
|
|
||||||
$: userCount = $licensing.userCount + userEmails.length
|
$: userCount = $licensing.userCount + userEmails.length
|
||||||
$: willExceed = licensing.willExceedUserLimit(userCount)
|
$: exceed = licensing.usersLimitExceeded(userCount)
|
||||||
|
|
||||||
$: importDisabled =
|
$: importDisabled =
|
||||||
!userEmails.length || !validEmails(userEmails) || !usersRole || willExceed
|
!userEmails.length || !validEmails(userEmails) || !usersRole || exceed
|
||||||
|
|
||||||
const validEmails = userEmails => {
|
const validEmails = userEmails => {
|
||||||
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
if ($admin.cloud && userEmails.length > MAX_USERS_UPLOAD_LIMIT) {
|
||||||
|
@ -93,7 +93,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if willExceed}
|
{#if exceed}
|
||||||
<div class="user-notification">
|
<div class="user-notification">
|
||||||
<Icon name="Info" />
|
<Icon name="Info" />
|
||||||
{capitalise($licensing.license.plan.type)} plan is limited to {$licensing.userLimit}
|
{capitalise($licensing.license.plan.type)} plan is limited to {$licensing.userLimit}
|
||||||
|
|
|
@ -268,8 +268,6 @@
|
||||||
notifications.error("Error fetching user group data")
|
notifications.error("Error fetching user group data")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let staticUserLimit = $licensing.license.quotas.usage.static.users.value
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding gap="M">
|
<Layout noPadding gap="M">
|
||||||
|
@ -278,7 +276,7 @@
|
||||||
<Body>Add users and control who gets access to your published apps</Body>
|
<Body>Add users and control who gets access to your published apps</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#if $licensing.warnUserLimit}
|
{#if $licensing.errUserLimit}
|
||||||
<InlineAlert
|
<InlineAlert
|
||||||
type="error"
|
type="error"
|
||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
|
@ -290,13 +288,9 @@
|
||||||
}}
|
}}
|
||||||
buttonText={isOwner ? "Upgrade" : "View plans"}
|
buttonText={isOwner ? "Upgrade" : "View plans"}
|
||||||
cta
|
cta
|
||||||
header={`Users will soon be limited to ${staticUserLimit}`}
|
header="Account de-activated"
|
||||||
message={`Our free plan is going to be limited to ${staticUserLimit} users in ${$licensing.userLimitDays}.
|
message="Due to the free plan user limit being exceeded, your account has been de-activated.
|
||||||
|
Upgrade your plan to re-activate your account."
|
||||||
This means any users exceeding the limit will be de-activated.
|
|
||||||
|
|
||||||
De-activated users will not able to access the builder or any published apps until you upgrade to one of our paid plans.
|
|
||||||
`}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { auth, admin } from "stores/portal"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import { StripeStatus } from "components/portal/licensing/constants"
|
import { StripeStatus } from "components/portal/licensing/constants"
|
||||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||||
import dayjs from "dayjs"
|
import { PlanModel } from "@budibase/types"
|
||||||
|
|
||||||
const UNLIMITED = -1
|
const UNLIMITED = -1
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ export const createLicensingStore = () => {
|
||||||
const DEFAULT = {
|
const DEFAULT = {
|
||||||
// navigation
|
// navigation
|
||||||
goToUpgradePage: () => {},
|
goToUpgradePage: () => {},
|
||||||
|
goToPricingPage: () => {},
|
||||||
// the top level license
|
// the top level license
|
||||||
license: undefined,
|
license: undefined,
|
||||||
isFreePlan: true,
|
isFreePlan: true,
|
||||||
|
@ -37,29 +38,37 @@ export const createLicensingStore = () => {
|
||||||
// user limits
|
// user limits
|
||||||
userCount: undefined,
|
userCount: undefined,
|
||||||
userLimit: undefined,
|
userLimit: undefined,
|
||||||
userLimitDays: undefined,
|
|
||||||
userLimitReached: false,
|
userLimitReached: false,
|
||||||
warnUserLimit: false,
|
errUserLimit: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const oneDayInMilliseconds = 86400000
|
const oneDayInMilliseconds = 86400000
|
||||||
|
|
||||||
const store = writable(DEFAULT)
|
const store = writable(DEFAULT)
|
||||||
|
|
||||||
function willReachUserLimit(userCount, userLimit) {
|
function usersLimitReached(userCount, userLimit) {
|
||||||
if (userLimit === UNLIMITED) {
|
if (userLimit === UNLIMITED) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return userCount >= userLimit
|
return userCount >= userLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
function willExceedUserLimit(userCount, userLimit) {
|
function usersLimitExceeded(userCount, userLimit) {
|
||||||
if (userLimit === UNLIMITED) {
|
if (userLimit === UNLIMITED) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return userCount > userLimit
|
return userCount > userLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function isCloud() {
|
||||||
|
let adminStore = get(admin)
|
||||||
|
if (!adminStore.loaded) {
|
||||||
|
await admin.init()
|
||||||
|
adminStore = get(admin)
|
||||||
|
}
|
||||||
|
return adminStore.cloud
|
||||||
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
init: async () => {
|
init: async () => {
|
||||||
actions.setNavigation()
|
actions.setNavigation()
|
||||||
|
@ -71,10 +80,14 @@ export const createLicensingStore = () => {
|
||||||
const goToUpgradePage = () => {
|
const goToUpgradePage = () => {
|
||||||
window.location.href = upgradeUrl
|
window.location.href = upgradeUrl
|
||||||
}
|
}
|
||||||
|
const goToPricingPage = () => {
|
||||||
|
window.open("https://budibase.com/pricing/", "_blank")
|
||||||
|
}
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
goToUpgradePage,
|
goToUpgradePage,
|
||||||
|
goToPricingPage,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -128,15 +141,15 @@ export const createLicensingStore = () => {
|
||||||
quotaUsage,
|
quotaUsage,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
actions.setUsageMetrics()
|
await actions.setUsageMetrics()
|
||||||
},
|
},
|
||||||
willReachUserLimit: userCount => {
|
usersLimitReached: userCount => {
|
||||||
return willReachUserLimit(userCount, get(store).userLimit)
|
return usersLimitReached(userCount, get(store).userLimit)
|
||||||
},
|
},
|
||||||
willExceedUserLimit(userCount) {
|
usersLimitExceeded(userCount) {
|
||||||
return willExceedUserLimit(userCount, get(store).userLimit)
|
return usersLimitExceeded(userCount, get(store).userLimit)
|
||||||
},
|
},
|
||||||
setUsageMetrics: () => {
|
setUsageMetrics: async () => {
|
||||||
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
||||||
const usage = get(store).quotaUsage
|
const usage = get(store).quotaUsage
|
||||||
const license = get(auth).user.license
|
const license = get(auth).user.license
|
||||||
|
@ -198,11 +211,13 @@ export const createLicensingStore = () => {
|
||||||
const userQuota = license.quotas.usage.static.users
|
const userQuota = license.quotas.usage.static.users
|
||||||
const userLimit = userQuota?.value
|
const userLimit = userQuota?.value
|
||||||
const userCount = usage.usageQuota.users
|
const userCount = usage.usageQuota.users
|
||||||
const userLimitReached = willReachUserLimit(userCount, userLimit)
|
const userLimitReached = usersLimitReached(userCount, userLimit)
|
||||||
const userLimitExceeded = willExceedUserLimit(userCount, userLimit)
|
const userLimitExceeded = usersLimitExceeded(userCount, userLimit)
|
||||||
const days = dayjs(userQuota?.startDate).diff(dayjs(), "day")
|
const isCloudAccount = await isCloud()
|
||||||
const userLimitDays = days > 1 ? `${days} days` : "1 day"
|
const errUserLimit =
|
||||||
const warnUserLimit = userQuota?.startDate && userLimitExceeded
|
isCloudAccount &&
|
||||||
|
license.plan.model === PlanModel.PER_USER &&
|
||||||
|
userLimitExceeded
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
return {
|
return {
|
||||||
|
@ -217,9 +232,8 @@ export const createLicensingStore = () => {
|
||||||
// user limits
|
// user limits
|
||||||
userCount,
|
userCount,
|
||||||
userLimit,
|
userLimit,
|
||||||
userLimitDays,
|
|
||||||
userLimitReached,
|
userLimitReached,
|
||||||
warnUserLimit,
|
errUserLimit,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue