Merge branch 'cheeks-lab-day-portal-redesign' of github.com:Budibase/budibase into feature/environment-variables

This commit is contained in:
mike12345567 2023-01-27 10:52:46 +00:00
commit 301bc2af8e
37 changed files with 412 additions and 320 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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 = {

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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 {

View File

@ -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 = [

View File

@ -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"

View File

@ -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;

View File

@ -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>

View File

@ -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"

View File

@ -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 {

View File

@ -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 {

View File

@ -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}

View File

@ -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">

View File

@ -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}

View File

@ -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 />

View File

@ -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;

View File

@ -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
}}
/>

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -103,6 +103,7 @@
allowEditColumns={false} allowEditColumns={false}
allowEditRows={false} allowEditRows={false}
allowSelectRows={false} allowSelectRows={false}
allowClickRows={false}
{customRenderers} {customRenderers}
/> />
</Layout> </Layout>

View File

@ -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 />

View File

@ -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 />

View File

@ -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;

View File

@ -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>

View File

@ -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

View File

@ -0,0 +1,4 @@
<script>
import { redirect } from "@roxi/routify"
$redirect("./users")
</script>

View File

@ -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;

View File

@ -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>

View File

@ -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} />

View File

@ -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"

View File

@ -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
})