Merge branch 'cheeks-lab-day-portal-redesign' of github.com:Budibase/budibase into feature/environment-variables
This commit is contained in:
commit
301bc2af8e
|
@ -6,7 +6,8 @@ services:
|
||||||
minio-service:
|
minio-service:
|
||||||
container_name: budi-minio-dev
|
container_name: budi-minio-dev
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
image: minio/minio
|
# Last version that supports the "fs" backend
|
||||||
|
image: minio/minio:RELEASE.2022-10-24T18-35-07Z
|
||||||
volumes:
|
volumes:
|
||||||
- minio_data:/data
|
- minio_data:/data
|
||||||
ports:
|
ports:
|
||||||
|
|
|
@ -5,7 +5,7 @@ let clickHandlers = []
|
||||||
* Handle a body click event
|
* Handle a body click event
|
||||||
*/
|
*/
|
||||||
const handleClick = event => {
|
const handleClick = event => {
|
||||||
// Ignore click if needed
|
// Ignore click if this is an ignored class
|
||||||
for (let className of ignoredClasses) {
|
for (let className of ignoredClasses) {
|
||||||
if (event.target.closest(className)) {
|
if (event.target.closest(className)) {
|
||||||
return
|
return
|
||||||
|
@ -14,9 +14,18 @@ const handleClick = event => {
|
||||||
|
|
||||||
// Process handlers
|
// Process handlers
|
||||||
clickHandlers.forEach(handler => {
|
clickHandlers.forEach(handler => {
|
||||||
if (!handler.element.contains(event.target)) {
|
if (handler.element.contains(event.target)) {
|
||||||
handler.callback?.(event)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore clicks for modals, unless the handler is registered from a modal
|
||||||
|
const sourceInModal = handler.element.closest(".spectrum-Modal") != null
|
||||||
|
const clickInModal = event.target.closest(".spectrum-Modal") != null
|
||||||
|
if (clickInModal && !sourceInModal) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.callback?.(event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
document.documentElement.addEventListener("click", handleClick, true)
|
document.documentElement.addEventListener("click", handleClick, true)
|
||||||
|
|
|
@ -3,7 +3,6 @@ export default function positionDropdown(
|
||||||
{ anchor, align, maxWidth, useAnchorWidth }
|
{ anchor, align, maxWidth, useAnchorWidth }
|
||||||
) {
|
) {
|
||||||
const update = () => {
|
const update = () => {
|
||||||
console.log("update")
|
|
||||||
const anchorBounds = anchor.getBoundingClientRect()
|
const anchorBounds = anchor.getBoundingClientRect()
|
||||||
const elementBounds = element.getBoundingClientRect()
|
const elementBounds = element.getBoundingClientRect()
|
||||||
let styles = {
|
let styles = {
|
||||||
|
|
|
@ -58,5 +58,6 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -112,8 +112,4 @@
|
||||||
.spectrum-Textfield {
|
.spectrum-Textfield {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
input:disabled {
|
|
||||||
color: var(--spectrum-global-color-gray-600) !important;
|
|
||||||
-webkit-text-fill-color: var(--spectrum-global-color-gray-600) !important;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
export let allowSelectRows
|
export let allowSelectRows
|
||||||
export let allowEditRows = true
|
export let allowEditRows = true
|
||||||
export let allowEditColumns = true
|
export let allowEditColumns = true
|
||||||
|
export let allowClickRows = true
|
||||||
export let selectedRows = []
|
export let selectedRows = []
|
||||||
export let customRenderers = []
|
export let customRenderers = []
|
||||||
export let disableSorting = false
|
export let disableSorting = false
|
||||||
|
@ -373,7 +374,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if sortedRows?.length}
|
{#if sortedRows?.length}
|
||||||
{#each sortedRows as row, idx}
|
{#each sortedRows as row, idx}
|
||||||
<div class="spectrum-Table-row">
|
<div class="spectrum-Table-row" class:clickable={allowClickRows}>
|
||||||
{#if showEditColumn}
|
{#if showEditColumn}
|
||||||
<div
|
<div
|
||||||
class:noBorderCheckbox={!showHeaderBorder}
|
class:noBorderCheckbox={!showHeaderBorder}
|
||||||
|
@ -566,8 +567,12 @@
|
||||||
/* Table rows */
|
/* Table rows */
|
||||||
.spectrum-Table-row {
|
.spectrum-Table-row {
|
||||||
display: contents;
|
display: contents;
|
||||||
|
cursor: auto;
|
||||||
}
|
}
|
||||||
.spectrum-Table-row:hover .spectrum-Table-cell {
|
.spectrum-Table-row.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.spectrum-Table-row.clickable:hover .spectrum-Table-cell {
|
||||||
background-color: var(--spectrum-global-color-gray-100);
|
background-color: var(--spectrum-global-color-gray-100);
|
||||||
}
|
}
|
||||||
.wrapper--quiet .spectrum-Table-row {
|
.wrapper--quiet .spectrum-Table-row {
|
||||||
|
|
|
@ -40,7 +40,6 @@
|
||||||
--rounded-medium: 8px;
|
--rounded-medium: 8px;
|
||||||
--rounded-large: 16px;
|
--rounded-large: 16px;
|
||||||
|
|
||||||
|
|
||||||
--font-sans: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI, "Inter",
|
--font-sans: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI, "Inter",
|
||||||
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||||
--font-accent: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI, "Inter",
|
--font-accent: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI, "Inter",
|
||||||
|
@ -92,6 +91,8 @@
|
||||||
--border-light-2: 2px var(--grey-3) solid;
|
--border-light-2: 2px var(--grey-3) solid;
|
||||||
--border-blue: 2px var(--blue) solid;
|
--border-blue: 2px var(--blue) solid;
|
||||||
--border-transparent: 2px transparent solid;
|
--border-transparent: 2px transparent solid;
|
||||||
|
|
||||||
|
--spectrum-alias-text-color-disabled: var(--spectrum-global-color-gray-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
export let rows = []
|
export let rows = []
|
||||||
export let schema = {}
|
export let schema = {}
|
||||||
export let allValid = false
|
export let allValid = true
|
||||||
export let displayColumn = null
|
export let displayColumn = null
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
let autoColumns = getAutoColumnInformation()
|
let autoColumns = getAutoColumnInformation()
|
||||||
let schema = {}
|
let schema = {}
|
||||||
let rows = []
|
let rows = []
|
||||||
let allValid = false
|
let allValid = true
|
||||||
let displayColumn = null
|
let displayColumn = null
|
||||||
|
|
||||||
function getAutoColumns() {
|
function getAutoColumns() {
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
title="Create Table"
|
title="Create Table"
|
||||||
confirmText="Create"
|
confirmText="Create"
|
||||||
onConfirm={saveTable}
|
onConfirm={saveTable}
|
||||||
disabled={error || !name || !allValid}
|
disabled={error || !name || (rows.length && !allValid)}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
data-cy="table-name-input"
|
data-cy="table-name-input"
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
bottom: var(--spacing-m);
|
bottom: var(--spacing-m);
|
||||||
right: var(--spacing-m);
|
right: var(--spacing-m);
|
||||||
border-radius: 55%;
|
border-radius: 55%;
|
||||||
|
z-index: 99999;
|
||||||
}
|
}
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -39,15 +39,21 @@
|
||||||
{#if showWarning}
|
{#if showWarning}
|
||||||
<Icon name="Alert" />
|
<Icon name="Alert" />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="heading header-item">
|
<Heading size="XS" weight="light">
|
||||||
<Heading size="XS" weight="light">{usage.name}</Heading>
|
<span class="nowrap">
|
||||||
</div>
|
{usage.name}
|
||||||
|
</span>
|
||||||
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
|
<Body size="S">
|
||||||
|
<span class="nowrap">
|
||||||
{#if unlimited}
|
{#if unlimited}
|
||||||
<Body size="S">{usage.used} / Unlimited</Body>
|
{usage.used} / Unlimited
|
||||||
{:else}
|
{:else}
|
||||||
<Body size="S">{usage.used} / {usage.total}</Body>
|
{usage.used} / {usage.total}
|
||||||
{/if}
|
{/if}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{#if unlimited}
|
{#if unlimited}
|
||||||
|
@ -89,13 +95,14 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
.header-container {
|
.header-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.heading {
|
.nowrap {
|
||||||
margin-top: 3px;
|
white-space: nowrap;
|
||||||
margin-left: 5px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -104,7 +104,7 @@
|
||||||
<Icon size="XL" name="ChevronDown" />
|
<Icon size="XL" name="ChevronDown" />
|
||||||
</div>
|
</div>
|
||||||
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
|
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
|
||||||
Update user information
|
My profile
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="LockClosed"
|
icon="LockClosed"
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import { organisation } from "stores/portal"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<img src={Logo} alt="Budibase Logo" on:click={() => $goto("./apps")} />
|
<img
|
||||||
|
src={$organisation.logoUrl || Logo}
|
||||||
|
alt="Budibase Logo"
|
||||||
|
on:click={() => $goto("./apps")}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
img {
|
img {
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
import UpgradeButton from "./UpgradeButton.svelte"
|
import UpgradeButton from "./UpgradeButton.svelte"
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import Logo from "./Logo.svelte"
|
import Logo from "./Logo.svelte"
|
||||||
|
import { menu } from "stores/portal"
|
||||||
|
|
||||||
export let visible = false
|
export let visible = false
|
||||||
export let menu
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -28,7 +28,20 @@
|
||||||
<Logo />
|
<Logo />
|
||||||
</div>
|
</div>
|
||||||
<SideNav>
|
<SideNav>
|
||||||
{#each menu as { title, href }}
|
{#each $menu as { title, href, subPages }}
|
||||||
|
{#if !subPages?.length}
|
||||||
|
<SideNavItem
|
||||||
|
text={title}
|
||||||
|
url={href}
|
||||||
|
active={$isActive(href)}
|
||||||
|
on:click={close}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{#each $menu as { title, href, subPages }}
|
||||||
|
{#if subPages?.length}
|
||||||
|
<div class="category">{title}</div>
|
||||||
|
{#each subPages as { title, href }}
|
||||||
<SideNavItem
|
<SideNavItem
|
||||||
text={title}
|
text={title}
|
||||||
url={href}
|
url={href}
|
||||||
|
@ -36,9 +49,11 @@
|
||||||
on:click={close}
|
on:click={close}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
</SideNav>
|
</SideNav>
|
||||||
<div>
|
<div>
|
||||||
<UpgradeButton />
|
<UpgradeButton on:click={close} />
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,6 +62,13 @@
|
||||||
.mobile-nav {
|
.mobile-nav {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.category {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
margin-left: var(--spacing-m);
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.mobile-nav-underlay {
|
.mobile-nav-underlay {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
{#if $admin.cloud && $auth?.user?.accountPortalAccess}
|
{#if $admin.cloud && $auth?.user?.accountPortalAccess}
|
||||||
<Button
|
<Button
|
||||||
cta
|
cta
|
||||||
|
on:click
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
$goto($admin.accountPortalUrl + "/portal/upgrade")
|
$goto($admin.accountPortalUrl + "/portal/upgrade")
|
||||||
}}
|
}}
|
||||||
|
@ -14,7 +15,12 @@
|
||||||
Upgrade
|
Upgrade
|
||||||
</Button>
|
</Button>
|
||||||
{:else if !$admin.cloud && $auth.isAdmin}
|
{:else if !$admin.cloud && $auth.isAdmin}
|
||||||
<Button cta on:click={() => $goto("/builder/portal/account/upgrade")}>
|
<Button
|
||||||
|
cta
|
||||||
|
size="S"
|
||||||
|
on:click={() => $goto("/builder/portal/account/upgrade")}
|
||||||
|
on:click
|
||||||
|
>
|
||||||
Upgrade
|
Upgrade
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
<ActionMenu align="right" dataCy="user-menu">
|
<ActionMenu align="right" dataCy="user-menu">
|
||||||
<div slot="control" class="user-dropdown">
|
<div slot="control" class="user-dropdown">
|
||||||
<Avatar size="L" initials={$auth.initials} url={$auth.user.pictureUrl} />
|
<Avatar size="M" initials={$auth.initials} url={$auth.user.pictureUrl} />
|
||||||
<Icon size="XL" name="ChevronDown" />
|
<Icon size="XL" name="ChevronDown" />
|
||||||
</div>
|
</div>
|
||||||
<MenuItem icon="Moon" on:click={() => themeModal.show()} dataCy="theme">
|
<MenuItem icon="Moon" on:click={() => themeModal.show()} dataCy="theme">
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { isActive, redirect, goto, url } from "@roxi/routify"
|
import { isActive, redirect, goto, url } from "@roxi/routify"
|
||||||
import { Icon, notifications, Tabs, Tab } from "@budibase/bbui"
|
import { Icon, notifications, Tabs, Tab } from "@budibase/bbui"
|
||||||
import { organisation, auth, admin as adminStore } from "stores/portal"
|
import { organisation, auth, menu } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
|
||||||
import UpgradeButton from "./_components/UpgradeButton.svelte"
|
import UpgradeButton from "./_components/UpgradeButton.svelte"
|
||||||
import MobileMenu from "./_components/MobileMenu.svelte"
|
import MobileMenu from "./_components/MobileMenu.svelte"
|
||||||
import Logo from "./_components/Logo.svelte"
|
import Logo from "./_components/Logo.svelte"
|
||||||
|
@ -13,10 +12,9 @@
|
||||||
let mobileMenuVisible = false
|
let mobileMenuVisible = false
|
||||||
let activeTab = "Apps"
|
let activeTab = "Apps"
|
||||||
|
|
||||||
$: menu = buildMenu($auth.isAdmin)
|
$: $url(), updateActiveTab($menu)
|
||||||
$: $url(), updateActiveTab()
|
|
||||||
|
|
||||||
const updateActiveTab = () => {
|
const updateActiveTab = menu => {
|
||||||
for (let entry of menu) {
|
for (let entry of menu) {
|
||||||
if ($isActive(entry.href)) {
|
if ($isActive(entry.href)) {
|
||||||
if (activeTab !== entry.title) {
|
if (activeTab !== entry.title) {
|
||||||
|
@ -27,55 +25,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildMenu = admin => {
|
|
||||||
// Standard user and developer pages
|
|
||||||
let menu = [
|
|
||||||
{
|
|
||||||
title: "Apps",
|
|
||||||
href: "/builder/portal/apps",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Plugins",
|
|
||||||
href: "/builder/portal/plugins",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// Admin only pages
|
|
||||||
if (admin) {
|
|
||||||
menu = [
|
|
||||||
{
|
|
||||||
title: "Apps",
|
|
||||||
href: "/builder/portal/apps",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Users",
|
|
||||||
href: "/builder/portal/users/users",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Plugins",
|
|
||||||
href: "/builder/portal/plugins",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Settings",
|
|
||||||
href: "/builder/portal/settings",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if allowed access to account section
|
|
||||||
if (
|
|
||||||
isEnabled(TENANT_FEATURE_FLAGS.LICENSING) &&
|
|
||||||
($auth?.user?.accountPortalAccess || (!$adminStore.cloud && admin))
|
|
||||||
) {
|
|
||||||
menu.push({
|
|
||||||
title: "Account",
|
|
||||||
href: "/builder/portal/account",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return menu
|
|
||||||
}
|
|
||||||
|
|
||||||
const showMobileMenu = () => (mobileMenuVisible = true)
|
const showMobileMenu = () => (mobileMenuVisible = true)
|
||||||
const hideMobileMenu = () => (mobileMenuVisible = false)
|
const hideMobileMenu = () => (mobileMenuVisible = false)
|
||||||
|
|
||||||
|
@ -104,7 +53,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<Tabs selected={activeTab}>
|
<Tabs selected={activeTab}>
|
||||||
{#each menu as { title, href }}
|
{#each $menu as { title, href }}
|
||||||
<Tab {title} on:click={() => $goto(href)} />
|
<Tab {title} on:click={() => $goto(href)} />
|
||||||
{/each}
|
{/each}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
@ -122,7 +71,7 @@
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<MobileMenu visible={mobileMenuVisible} {menu} on:close={hideMobileMenu} />
|
<MobileMenu visible={mobileMenuVisible} on:close={hideMobileMenu} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -1,40 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import { url, isActive } from "@roxi/routify"
|
import { isActive } from "@roxi/routify"
|
||||||
import { Page } from "@budibase/bbui"
|
import { Page } from "@budibase/bbui"
|
||||||
import { Content, SideNav, SideNavItem } from "components/portal/page"
|
import { Content, SideNav, SideNavItem } from "components/portal/page"
|
||||||
import { admin, auth } from "stores/portal"
|
import { menu } from "stores/portal"
|
||||||
|
|
||||||
|
$: pages = $menu.find(x => x.title === "Account").subPages
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page narrow>
|
<Page narrow>
|
||||||
<Content>
|
<Content>
|
||||||
<div slot="side-nav">
|
<div slot="side-nav">
|
||||||
<SideNav>
|
<SideNav>
|
||||||
<!-- Always show usage in self-host or cloud if licensing enabled-->
|
{#each pages as { title, href }}
|
||||||
<SideNavItem
|
<SideNavItem text={title} url={href} active={$isActive(href)} />
|
||||||
text="Usage"
|
{/each}
|
||||||
url={$url("./usage")}
|
|
||||||
active={$isActive("./usage")}
|
|
||||||
/>
|
|
||||||
<!-- Show the relevant hosting upgrade page-->
|
|
||||||
{#if $admin.cloud && $auth?.user?.accountPortalAccess}
|
|
||||||
<SideNavItem
|
|
||||||
text="Upgrade"
|
|
||||||
url={$admin.accountPortalUrl + "/portal/upgrade"}
|
|
||||||
/>
|
|
||||||
{:else if !$admin.cloud && admin}
|
|
||||||
<SideNavItem
|
|
||||||
text="Upgrade"
|
|
||||||
url={$url("./upgrade")}
|
|
||||||
active={$isActive("./upgrade")}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<!-- Show the billing page to licensed account holders in cloud -->
|
|
||||||
{#if $auth?.user?.accountPortalAccess && $auth.user.account.stripeCustomerId}
|
|
||||||
<SideNavItem
|
|
||||||
text="Billing"
|
|
||||||
url={$admin.accountPortalUrl + "/portal/billing"}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</SideNav>
|
</SideNav>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -32,6 +32,8 @@
|
||||||
$: license = $auth.user?.license
|
$: license = $auth.user?.license
|
||||||
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
||||||
$: quotaReset = quotaUsage?.quotaReset
|
$: quotaReset = quotaUsage?.quotaReset
|
||||||
|
$: canManagePlan =
|
||||||
|
($admin.cloud && accountPortalAccess) || (!$admin.cloud && $auth.isAdmin)
|
||||||
|
|
||||||
const setMonthlyUsage = () => {
|
const setMonthlyUsage = () => {
|
||||||
monthlyUsage = []
|
monthlyUsage = []
|
||||||
|
@ -184,10 +186,15 @@
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
{#if canManagePlan}
|
||||||
<Body>
|
<Body>
|
||||||
To upgrade your plan and usage limits visit your
|
To upgrade your plan and usage limits visit your
|
||||||
<Link size="L" on:click={goToAccountPortal}>account</Link>.
|
<Link size="L" on:click={goToAccountPortal}>account</Link>.
|
||||||
</Body>
|
</Body>
|
||||||
|
{:else}
|
||||||
|
<Body>Contact your account holder to upgrade your plan.</Body>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<DashCard
|
<DashCard
|
||||||
description="YOUR CURRENT PLAN"
|
description="YOUR CURRENT PLAN"
|
||||||
title={planTitle()}
|
title={planTitle()}
|
||||||
|
@ -242,6 +249,7 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 40px;
|
gap: 40px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.column {
|
.column {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
<script>
|
|
||||||
import { PickerDropdown } from "@budibase/bbui"
|
|
||||||
import { groups } from "stores/portal"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
let filter = null
|
|
||||||
$: filteredGroups = !filter
|
|
||||||
? $groups
|
|
||||||
: $groups.filter(group =>
|
|
||||||
group.name?.toLowerCase().includes(filter.toLowerCase())
|
|
||||||
)
|
|
||||||
|
|
||||||
$: optionSections = {
|
|
||||||
groups: {
|
|
||||||
data: filteredGroups,
|
|
||||||
getLabel: group => group.name,
|
|
||||||
getValue: group => group._id,
|
|
||||||
getIcon: group => group.icon,
|
|
||||||
getColour: group => group.color,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
$: onChange = selected => {
|
|
||||||
const { detail } = selected
|
|
||||||
if (!detail || Object.keys(detail).length == 0) {
|
|
||||||
dispatch("change", null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupSelected = $groups.find(x => x._id === detail)
|
|
||||||
const appRoleIds = groupSelected?.roles
|
|
||||||
? Object.keys(groupSelected?.roles)
|
|
||||||
: []
|
|
||||||
dispatch("change", appRoleIds)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<PickerDropdown
|
|
||||||
autocomplete
|
|
||||||
bind:searchTerm={filter}
|
|
||||||
primaryOptions={optionSections}
|
|
||||||
placeholder={"Filter by access"}
|
|
||||||
on:pickprimary={onChange}
|
|
||||||
on:closed={() => {
|
|
||||||
filter = null
|
|
||||||
}}
|
|
||||||
/>
|
|
|
@ -346,6 +346,10 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.app-actions :global(.spectrum-Textfield) {
|
||||||
|
max-width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-table {
|
.app-table {
|
||||||
|
@ -380,6 +384,9 @@
|
||||||
margin-top: var(--spacing-xl);
|
margin-top: var(--spacing-xl);
|
||||||
margin-bottom: calc(-1 * var(--spacing-m));
|
margin-bottom: calc(-1 * var(--spacing-m));
|
||||||
}
|
}
|
||||||
|
.app-actions :global(.spectrum-Textfield) {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
/* Hide download apps icon */
|
/* Hide download apps icon */
|
||||||
.app-actions :global(> .spectrum-Icon) {
|
.app-actions :global(> .spectrum-Icon) {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -260,10 +260,16 @@
|
||||||
.desktop {
|
.desktop {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
.mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.desktop {
|
.desktop {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.mobile {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let appUsers = []
|
export let appUsers = []
|
||||||
|
export let showUsers = false
|
||||||
|
export let showGroups = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const usersFetch = fetchData({
|
const usersFetch = fetchData({
|
||||||
|
@ -41,7 +43,8 @@
|
||||||
$: availableGroups = getAvailableGroups($groups, app.appId, search, data)
|
$: availableGroups = getAvailableGroups($groups, app.appId, search, data)
|
||||||
$: valid = data?.length && !data?.some(x => !x.id?.length || !x.role?.length)
|
$: valid = data?.length && !data?.some(x => !x.id?.length || !x.role?.length)
|
||||||
$: optionSections = {
|
$: optionSections = {
|
||||||
...($licensing.groupsEnabled &&
|
...(showGroups &&
|
||||||
|
$licensing.groupsEnabled &&
|
||||||
availableGroups.length && {
|
availableGroups.length && {
|
||||||
["User groups"]: {
|
["User groups"]: {
|
||||||
data: availableGroups,
|
data: availableGroups,
|
||||||
|
@ -51,6 +54,7 @@
|
||||||
getColour: group => group.color,
|
getColour: group => group.color,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
...(showUsers && {
|
||||||
users: {
|
users: {
|
||||||
data: availableUsers,
|
data: availableUsers,
|
||||||
getLabel: user => user.email,
|
getLabel: user => user.email,
|
||||||
|
@ -58,6 +62,7 @@
|
||||||
getIcon: user => user.icon,
|
getIcon: user => user.icon,
|
||||||
getColour: user => user.color,
|
getColour: user => user.color,
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
const addData = async appData => {
|
const addData = async appData => {
|
||||||
|
@ -139,7 +144,7 @@
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
size="M"
|
size="M"
|
||||||
title="Assign users to your app"
|
title="Assign access to your app"
|
||||||
confirmText="Done"
|
confirmText="Done"
|
||||||
cancelText="Cancel"
|
cancelText="Cancel"
|
||||||
onConfirm={() => addData(data)}
|
onConfirm={() => addData(data)}
|
||||||
|
@ -185,7 +190,7 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
{/if}
|
{/if}
|
||||||
<div>
|
<div>
|
||||||
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
|
<ActionButton on:click={addNewInput} icon="Add">Add more</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
},
|
},
|
||||||
role: {
|
role: {
|
||||||
displayName: "Access",
|
displayName: "Access",
|
||||||
width: "160px",
|
width: "150px",
|
||||||
borderLeft: true,
|
borderLeft: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,8 @@
|
||||||
let assignmentModal
|
let assignmentModal
|
||||||
let appGroups
|
let appGroups
|
||||||
let appUsers
|
let appUsers
|
||||||
|
let showAddUsers = false
|
||||||
|
let showAddGroups = false
|
||||||
|
|
||||||
$: app = $overview.selectedApp
|
$: app = $overview.selectedApp
|
||||||
$: devAppId = app.devId
|
$: devAppId = app.devId
|
||||||
|
@ -153,6 +155,18 @@
|
||||||
await usersFetch.refresh()
|
await usersFetch.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showUsersModal = () => {
|
||||||
|
showAddUsers = true
|
||||||
|
showAddGroups = false
|
||||||
|
assignmentModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const showGroupsModal = () => {
|
||||||
|
showAddUsers = false
|
||||||
|
showAddGroups = true
|
||||||
|
assignmentModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
setContext("roles", {
|
setContext("roles", {
|
||||||
updateRole,
|
updateRole,
|
||||||
removeRole,
|
removeRole,
|
||||||
|
@ -178,7 +192,7 @@
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<Heading size="S">Users</Heading>
|
<Heading size="S">Users</Heading>
|
||||||
<Button secondary on:click={assignmentModal.show}>Assign user</Button>
|
<Button cta on:click={showUsersModal}>Assign user</Button>
|
||||||
</div>
|
</div>
|
||||||
<Table
|
<Table
|
||||||
customPlaceholder
|
customPlaceholder
|
||||||
|
@ -203,13 +217,11 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $usersFetch.loaded && $licensing.groupsEnabled && appGroups.length}
|
{#if $usersFetch.loaded && $licensing.groupsEnabled}
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<Heading size="S">Groups</Heading>
|
<Heading size="S">Groups</Heading>
|
||||||
<Button secondary on:click={assignmentModal.show}>
|
<Button cta on:click={showGroupsModal}>Assign group</Button>
|
||||||
Assign group
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<Table
|
<Table
|
||||||
customPlaceholder
|
customPlaceholder
|
||||||
|
@ -228,7 +240,13 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={assignmentModal}>
|
<Modal bind:this={assignmentModal}>
|
||||||
<AssignmentModal {app} {appUsers} on:update={usersFetch.refresh} />
|
<AssignmentModal
|
||||||
|
{app}
|
||||||
|
{appUsers}
|
||||||
|
on:update={usersFetch.refresh}
|
||||||
|
showGroups={showAddGroups}
|
||||||
|
showUsers={showAddUsers}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
ActionButton,
|
|
||||||
Button,
|
Button,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Divider,
|
Divider,
|
||||||
|
@ -90,7 +89,7 @@
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
displayName: null,
|
displayName: null,
|
||||||
width: "5%",
|
width: "auto",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,9 +245,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ActionButton on:click={modal.show} icon="SaveAsFloppy">
|
<Button cta on:click={modal.show}>Create new backup</Button>
|
||||||
Create new backup
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table">
|
<div class="table">
|
||||||
|
|
|
@ -103,6 +103,7 @@
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
allowSelectRows={false}
|
allowSelectRows={false}
|
||||||
|
allowClickRows={false}
|
||||||
{customRenderers}
|
{customRenderers}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -1,43 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
import { url, isActive } from "@roxi/routify"
|
import { isActive } from "@roxi/routify"
|
||||||
import { Page } from "@budibase/bbui"
|
import { Page } from "@budibase/bbui"
|
||||||
import { Content, SideNav, SideNavItem } from "components/portal/page"
|
import { Content, SideNav, SideNavItem } from "components/portal/page"
|
||||||
import { admin } from "stores/portal"
|
import { menu } from "stores/portal"
|
||||||
|
|
||||||
$: wide = $isActive("./email/:template")
|
$: wide = $isActive("./email/:template")
|
||||||
|
$: pages = $menu.find(x => x.title === "Settings").subPages
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page>
|
<Page>
|
||||||
<Content narrow={!wide}>
|
<Content narrow={!wide}>
|
||||||
<div slot="side-nav">
|
<div slot="side-nav">
|
||||||
<SideNav>
|
<SideNav>
|
||||||
<SideNavItem
|
{#each pages as { title, href }}
|
||||||
text="Auth"
|
<SideNavItem text={title} url={href} active={$isActive(href)} />
|
||||||
url={$url("./auth")}
|
{/each}
|
||||||
active={$isActive("./auth")}
|
|
||||||
/>
|
|
||||||
<SideNavItem
|
|
||||||
text="Email"
|
|
||||||
url={$url("./email")}
|
|
||||||
active={$isActive("./email")}
|
|
||||||
/>
|
|
||||||
<SideNavItem
|
|
||||||
text="Organisation"
|
|
||||||
url={$url("./organisation")}
|
|
||||||
active={$isActive("./organisation")}
|
|
||||||
/>
|
|
||||||
<SideNavItem
|
|
||||||
text="Environment"
|
|
||||||
url={$url("./environment")}
|
|
||||||
active={$isActive("./environment")}
|
|
||||||
/>
|
|
||||||
{#if !$admin.cloud}
|
|
||||||
<SideNavItem
|
|
||||||
text="Version"
|
|
||||||
url={$url("./version")}
|
|
||||||
active={$isActive("./version")}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</SideNav>
|
</SideNav>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -1,25 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
import { Page } from "@budibase/bbui"
|
import { Page } from "@budibase/bbui"
|
||||||
import { SideNav, SideNavItem, Content } from "components/portal/page"
|
import { SideNav, SideNavItem, Content } from "components/portal/page"
|
||||||
import { isActive, url } from "@roxi/routify"
|
import { isActive } from "@roxi/routify"
|
||||||
|
import { menu } from "stores/portal"
|
||||||
|
|
||||||
$: wide = $isActive("./users/index") || $isActive("./groups/index")
|
$: wide = $isActive("./users/index") || $isActive("./groups/index")
|
||||||
|
$: pages = $menu.find(x => x.title === "Users").subPages
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page>
|
<Page>
|
||||||
<Content narrow={!wide}>
|
<Content narrow={!wide}>
|
||||||
<div slot="side-nav">
|
<div slot="side-nav">
|
||||||
<SideNav>
|
<SideNav>
|
||||||
<SideNavItem
|
{#each pages as { title, href }}
|
||||||
text="Users"
|
<SideNavItem text={title} url={href} active={$isActive(href)} />
|
||||||
url={$url("./users")}
|
{/each}
|
||||||
active={$isActive("./users")}
|
|
||||||
/>
|
|
||||||
<SideNavItem
|
|
||||||
text="Groups"
|
|
||||||
url={$url("./groups")}
|
|
||||||
active={$isActive("./groups")}
|
|
||||||
/>
|
|
||||||
</SideNav>
|
</SideNav>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
import { users, apps, groups } from "stores/portal"
|
import { users, apps, groups, auth } from "stores/portal"
|
||||||
import { onMount, setContext } from "svelte"
|
import { onMount, setContext } from "svelte"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
@ -70,6 +70,7 @@
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let editModal, deleteModal
|
let editModal, deleteModal
|
||||||
|
|
||||||
|
$: readonly = !$auth.isAdmin
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: fetchUsers(page, searchTerm)
|
$: fetchUsers(page, searchTerm)
|
||||||
$: group = $groups.find(x => x._id === groupId)
|
$: group = $groups.find(x => x._id === groupId)
|
||||||
|
@ -84,7 +85,6 @@
|
||||||
...app,
|
...app,
|
||||||
role: group?.roles?.[apps.getProdAppID(app.devId)],
|
role: group?.roles?.[apps.getProdAppID(app.devId)],
|
||||||
}))
|
}))
|
||||||
$: console.log(groupApps)
|
|
||||||
$: {
|
$: {
|
||||||
if (loaded && !group?._id) {
|
if (loaded && !group?._id) {
|
||||||
$goto("./")
|
$goto("./")
|
||||||
|
@ -164,6 +164,7 @@
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<GroupIcon {group} size="L" />
|
<GroupIcon {group} size="L" />
|
||||||
<Heading>{group?.name}</Heading>
|
<Heading>{group?.name}</Heading>
|
||||||
|
{#if !readonly}
|
||||||
<ActionMenu align="right">
|
<ActionMenu align="right">
|
||||||
<span slot="control">
|
<span slot="control">
|
||||||
<Icon hoverable name="More" />
|
<Icon hoverable name="More" />
|
||||||
|
@ -175,13 +176,16 @@
|
||||||
Delete
|
Delete
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<Heading size="S">Users</Heading>
|
<Heading size="S">Users</Heading>
|
||||||
<div bind:this={popoverAnchor}>
|
<div bind:this={popoverAnchor}>
|
||||||
<Button on:click={popover.show()} cta>Add user</Button>
|
<Button disabled={readonly} on:click={popover.show()} cta
|
||||||
|
>Add user</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
||||||
<UserGroupPicker
|
<UserGroupPicker
|
||||||
|
@ -246,7 +250,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-l);
|
||||||
}
|
}
|
||||||
.header :global(.spectrum-Heading) {
|
.header :global(.spectrum-Heading) {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton } from "@budibase/bbui"
|
import { ActionButton } from "@budibase/bbui"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
|
@ -12,4 +13,6 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton size="S" on:click={onClick}>Remove</ActionButton>
|
<ActionButton disabled={!$auth.isAdmin} size="S" on:click={onClick}>
|
||||||
|
Remove
|
||||||
|
</ActionButton>
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
{ column: "roles", component: GroupAppsTableRenderer },
|
{ column: "roles", component: GroupAppsTableRenderer },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
$: readonly = !$auth.isAdmin
|
||||||
$: schema = {
|
$: schema = {
|
||||||
name: { displayName: "Group", width: "2fr", minWidth: "200px" },
|
name: { displayName: "Group", width: "2fr", minWidth: "200px" },
|
||||||
users: { sortable: false, width: "1fr" },
|
users: { sortable: false, width: "1fr" },
|
||||||
|
@ -108,7 +109,9 @@
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
{#if $licensing.groupsEnabled}
|
{#if $licensing.groupsEnabled}
|
||||||
<!--Show the group create button-->
|
<!--Show the group create button-->
|
||||||
<Button cta on:click={showCreateGroupModal}>Add group</Button>
|
<Button disabled={readonly} cta on:click={showCreateGroupModal}>
|
||||||
|
Add group
|
||||||
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button
|
<Button
|
||||||
primary
|
primary
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<script>
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
$redirect("./users")
|
||||||
|
</script>
|
|
@ -81,6 +81,7 @@
|
||||||
let user
|
let user
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
|
$: readonly = !$auth.isAdmin
|
||||||
$: fullName = user?.firstName ? user?.firstName + " " + user?.lastName : ""
|
$: fullName = user?.firstName ? user?.firstName + " " + user?.lastName : ""
|
||||||
$: privileged = user?.admin?.global || user?.builder?.global
|
$: privileged = user?.admin?.global || user?.builder?.global
|
||||||
$: nameLabel = getNameLabel(user)
|
$: nameLabel = getNameLabel(user)
|
||||||
|
@ -235,8 +236,7 @@
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div>
|
<div class="user-info">
|
||||||
<div style="display: flex;">
|
|
||||||
<Avatar size="XXL" {initials} />
|
<Avatar size="XXL" {initials} />
|
||||||
<div class="subtitle">
|
<div class="subtitle">
|
||||||
<Heading size="M">{nameLabel}</Heading>
|
<Heading size="M">{nameLabel}</Heading>
|
||||||
|
@ -245,8 +245,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{#if userId !== $auth.user?._id && !readonly}
|
||||||
{#if userId !== $auth.user?._id}
|
|
||||||
<div>
|
<div>
|
||||||
<ActionMenu align="right">
|
<ActionMenu align="right">
|
||||||
<span slot="control">
|
<span slot="control">
|
||||||
|
@ -271,17 +270,26 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">First name</Label>
|
<Label size="L">First name</Label>
|
||||||
<Input value={user?.firstName} on:blur={updateUserFirstName} />
|
<Input
|
||||||
|
disabled={readonly}
|
||||||
|
value={user?.firstName}
|
||||||
|
on:blur={updateUserFirstName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Last name</Label>
|
<Label size="L">Last name</Label>
|
||||||
<Input value={user?.lastName} on:blur={updateUserLastName} />
|
<Input
|
||||||
|
disabled={readonly}
|
||||||
|
value={user?.lastName}
|
||||||
|
on:blur={updateUserLastName}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- don't let a user remove the privileges that let them be here -->
|
<!-- don't let a user remove the privileges that let them be here -->
|
||||||
{#if userId !== $auth.user._id}
|
{#if userId !== $auth.user._id}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Role</Label>
|
<Label size="L">Role</Label>
|
||||||
<Select
|
<Select
|
||||||
|
disabled={readonly}
|
||||||
value={globalRole}
|
value={globalRole}
|
||||||
options={Constants.BudibaseRoleOptions}
|
options={Constants.BudibaseRoleOptions}
|
||||||
on:change={updateUserRole}
|
on:change={updateUserRole}
|
||||||
|
@ -297,7 +305,9 @@
|
||||||
<div class="tableTitle">
|
<div class="tableTitle">
|
||||||
<Heading size="S">Groups</Heading>
|
<Heading size="S">Groups</Heading>
|
||||||
<div bind:this={popoverAnchor}>
|
<div bind:this={popoverAnchor}>
|
||||||
<Button on:click={popover.show()} secondary>Add to group</Button>
|
<Button disabled={readonly} on:click={popover.show()} secondary>
|
||||||
|
Add to group
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
|
||||||
<UserGroupPicker
|
<UserGroupPicker
|
||||||
|
@ -375,13 +385,18 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
.tableTitle {
|
.tableTitle {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
.subtitle {
|
.subtitle {
|
||||||
padding: 0 0 0 var(--spacing-m);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton } from "@budibase/bbui"
|
import { ActionButton } from "@budibase/bbui"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
|
@ -12,4 +13,6 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton size="S" on:click={onClick}>Remove</ActionButton>
|
<ActionButton disabled={!$auth.isAdmin} size="S" on:click={onClick}>
|
||||||
|
Remove
|
||||||
|
</ActionButton>
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
let loaded = false
|
let groupsLoaded = !$licensing.groupsEnabled || $groups?.length
|
||||||
let enrichedUsers = []
|
let enrichedUsers = []
|
||||||
let createUserModal,
|
let createUserModal,
|
||||||
inviteConfirmationModal,
|
inviteConfirmationModal,
|
||||||
|
@ -52,6 +52,7 @@
|
||||||
]
|
]
|
||||||
let userData = []
|
let userData = []
|
||||||
|
|
||||||
|
$: readonly = !$auth.isAdmin
|
||||||
$: debouncedUpdateFetch(searchEmail)
|
$: debouncedUpdateFetch(searchEmail)
|
||||||
$: schema = {
|
$: schema = {
|
||||||
email: {
|
email: {
|
||||||
|
@ -205,17 +206,15 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
loaded = false
|
|
||||||
await groups.actions.init()
|
await groups.actions.init()
|
||||||
loaded = true
|
groupsLoaded = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error fetching User Group data")
|
notifications.error("Error fetching user group data")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loaded && $fetch.loaded}
|
<Layout noPadding gap="M">
|
||||||
<Layout noPadding gap="M">
|
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading>Users</Heading>
|
<Heading>Users</Heading>
|
||||||
<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>
|
||||||
|
@ -223,10 +222,17 @@
|
||||||
<Divider />
|
<Divider />
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button dataCy="add-user" on:click={createUserModal.show} cta>
|
<Button
|
||||||
|
disabled={readonly}
|
||||||
|
dataCy="add-user"
|
||||||
|
on:click={createUserModal.show}
|
||||||
|
cta
|
||||||
|
>
|
||||||
Add users
|
Add users
|
||||||
</Button>
|
</Button>
|
||||||
<Button on:click={importUsersModal.show} secondary>Import</Button>
|
<Button disabled={readonly} on:click={importUsersModal.show} secondary>
|
||||||
|
Import
|
||||||
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<div class="controls-right">
|
<div class="controls-right">
|
||||||
<Search bind:value={searchEmail} placeholder="Search" />
|
<Search bind:value={searchEmail} placeholder="Search" />
|
||||||
|
@ -247,8 +253,9 @@
|
||||||
data={enrichedUsers}
|
data={enrichedUsers}
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
allowSelectRows={true}
|
allowSelectRows={!readonly}
|
||||||
{customRenderers}
|
{customRenderers}
|
||||||
|
loading={!$fetch.loaded || !groupsLoaded}
|
||||||
/>
|
/>
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
<Pagination
|
<Pagination
|
||||||
|
@ -259,8 +266,7 @@
|
||||||
goToNextPage={fetch.nextPage}
|
goToNextPage={fetch.nextPage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Modal bind:this={createUserModal}>
|
<Modal bind:this={createUserModal}>
|
||||||
<AddUserModal {showOnboardingTypeModal} />
|
<AddUserModal {showOnboardingTypeModal} />
|
||||||
|
|
|
@ -12,3 +12,4 @@ export { plugins } from "./plugins"
|
||||||
export { backups } from "./backups"
|
export { backups } from "./backups"
|
||||||
export { overview } from "./overview"
|
export { overview } from "./overview"
|
||||||
export { environment } from "./environment"
|
export { environment } from "./environment"
|
||||||
|
export { menu } from "./menu"
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { derived } from "svelte/store"
|
||||||
|
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||||
|
import { admin } from "./admin"
|
||||||
|
import { auth } from "./auth"
|
||||||
|
|
||||||
|
export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
|
// Determine user sub pages
|
||||||
|
let userSubPages = [
|
||||||
|
{
|
||||||
|
title: "Users",
|
||||||
|
href: "/builder/portal/users/users",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if (isEnabled(TENANT_FEATURE_FLAGS.USER_GROUPS)) {
|
||||||
|
userSubPages.push({
|
||||||
|
title: "Groups",
|
||||||
|
href: "/builder/portal/users/groups",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages that all devs and admins can access
|
||||||
|
let menu = [
|
||||||
|
{
|
||||||
|
title: "Apps",
|
||||||
|
href: "/builder/portal/apps",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Users",
|
||||||
|
href: "/builder/portal/users",
|
||||||
|
subPages: userSubPages,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Plugins",
|
||||||
|
href: "/builder/portal/plugins",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Add settings page for admins
|
||||||
|
if ($auth.isAdmin) {
|
||||||
|
let settingsSubPages = [
|
||||||
|
{
|
||||||
|
title: "Auth",
|
||||||
|
href: "/builder/portal/settings/auth",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Email",
|
||||||
|
href: "/builder/portal/settings/email",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Organisation",
|
||||||
|
href: "/builder/portal/settings/organisation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Environment",
|
||||||
|
href: "/builder/portal/settings/environment",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if (!$admin.cloud) {
|
||||||
|
settingsSubPages.push({
|
||||||
|
title: "Version",
|
||||||
|
href: "/builder/portal/settings/version",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
menu.push({
|
||||||
|
title: "Settings",
|
||||||
|
href: "/builder/portal/settings",
|
||||||
|
subPages: settingsSubPages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add account page
|
||||||
|
if (isEnabled(TENANT_FEATURE_FLAGS.LICENSING)) {
|
||||||
|
let accountSubPages = [
|
||||||
|
{
|
||||||
|
title: "Usage",
|
||||||
|
href: "/builder/portal/account/usage",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if ($admin.cloud && $auth?.user?.accountPortalAccess) {
|
||||||
|
accountSubPages.push({
|
||||||
|
title: "Upgrade",
|
||||||
|
href: $admin.accountPortalUrl + "/portal/upgrade",
|
||||||
|
})
|
||||||
|
} else if (!$admin.cloud && $auth.isAdmin) {
|
||||||
|
accountSubPages.push({
|
||||||
|
title: "Upgrade",
|
||||||
|
href: "/builder/portal/account/upgrade",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
$auth?.user?.accountPortalAccess &&
|
||||||
|
$auth.user.account.stripeCustomerId
|
||||||
|
) {
|
||||||
|
accountSubPages.push({
|
||||||
|
title: "Billing",
|
||||||
|
href: $admin.accountPortalUrl + "/portal/billing",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
menu.push({
|
||||||
|
title: "Account",
|
||||||
|
href: "/builder/portal/account",
|
||||||
|
subPages: accountSubPages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return menu
|
||||||
|
})
|
Loading…
Reference in New Issue