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:
container_name: budi-minio-dev
restart: on-failure
image: minio/minio
# Last version that supports the "fs" backend
image: minio/minio:RELEASE.2022-10-24T18-35-07Z
volumes:
- minio_data:/data
ports:

View File

@ -5,7 +5,7 @@ let clickHandlers = []
* Handle a body click event
*/
const handleClick = event => {
// Ignore click if needed
// Ignore click if this is an ignored class
for (let className of ignoredClasses) {
if (event.target.closest(className)) {
return
@ -14,9 +14,18 @@ const handleClick = event => {
// Process handlers
clickHandlers.forEach(handler => {
if (!handler.element.contains(event.target)) {
handler.callback?.(event)
if (handler.element.contains(event.target)) {
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)

View File

@ -3,7 +3,6 @@ export default function positionDropdown(
{ anchor, align, maxWidth, useAnchorWidth }
) {
const update = () => {
console.log("update")
const anchorBounds = anchor.getBoundingClientRect()
const elementBounds = element.getBoundingClientRect()
let styles = {

View File

@ -58,5 +58,6 @@
overflow: hidden;
user-select: none;
text-transform: uppercase;
flex-shrink: 0;
}
</style>

View File

@ -112,8 +112,4 @@
.spectrum-Textfield {
width: 100%;
}
input:disabled {
color: var(--spectrum-global-color-gray-600) !important;
-webkit-text-fill-color: var(--spectrum-global-color-gray-600) !important;
}
</style>

View File

@ -33,6 +33,7 @@
export let allowSelectRows
export let allowEditRows = true
export let allowEditColumns = true
export let allowClickRows = true
export let selectedRows = []
export let customRenderers = []
export let disableSorting = false
@ -373,7 +374,7 @@
{/if}
{#if sortedRows?.length}
{#each sortedRows as row, idx}
<div class="spectrum-Table-row">
<div class="spectrum-Table-row" class:clickable={allowClickRows}>
{#if showEditColumn}
<div
class:noBorderCheckbox={!showHeaderBorder}
@ -566,8 +567,12 @@
/* Table rows */
.spectrum-Table-row {
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);
}
.wrapper--quiet .spectrum-Table-row {

View File

@ -40,7 +40,6 @@
--rounded-medium: 8px;
--rounded-large: 16px;
--font-sans: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI, "Inter",
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
--font-accent: "Source Sans Pro", -apple-system, BlinkMacSystemFont, Segoe UI, "Inter",
@ -92,6 +91,8 @@
--border-light-2: 2px var(--grey-3) solid;
--border-blue: 2px var(--blue) solid;
--border-transparent: 2px transparent solid;
--spectrum-alias-text-color-disabled: var(--spectrum-global-color-gray-600);
}
a {

View File

@ -14,7 +14,7 @@
export let rows = []
export let schema = {}
export let allValid = false
export let allValid = true
export let displayColumn = null
const typeOptions = [

View File

@ -33,7 +33,7 @@
let autoColumns = getAutoColumnInformation()
let schema = {}
let rows = []
let allValid = false
let allValid = true
let displayColumn = null
function getAutoColumns() {
@ -99,7 +99,7 @@
title="Create Table"
confirmText="Create"
onConfirm={saveTable}
disabled={error || !name || !allValid}
disabled={error || !name || (rows.length && !allValid)}
>
<Input
data-cy="table-name-input"

View File

@ -31,6 +31,7 @@
bottom: var(--spacing-m);
right: var(--spacing-m);
border-radius: 55%;
z-index: 99999;
}
.hidden {
display: none;

View File

@ -39,15 +39,21 @@
{#if showWarning}
<Icon name="Alert" />
{/if}
<div class="heading header-item">
<Heading size="XS" weight="light">{usage.name}</Heading>
</div>
<Heading size="XS" weight="light">
<span class="nowrap">
{usage.name}
</span>
</Heading>
</div>
{#if unlimited}
<Body size="S">{usage.used} / Unlimited</Body>
{:else}
<Body size="S">{usage.used} / {usage.total}</Body>
{/if}
<Body size="S">
<span class="nowrap">
{#if unlimited}
{usage.used} / Unlimited
{:else}
{usage.used} / {usage.total}
{/if}
</span>
</Body>
</div>
<div>
{#if unlimited}
@ -89,13 +95,14 @@
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 12px;
gap: var(--spacing-m);
}
.header-container {
display: flex;
}
.heading {
margin-top: 3px;
margin-left: 5px;
.nowrap {
white-space: nowrap;
}
</style>

View File

@ -104,7 +104,7 @@
<Icon size="XL" name="ChevronDown" />
</div>
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
Update user information
My profile
</MenuItem>
<MenuItem
icon="LockClosed"

View File

@ -1,9 +1,14 @@
<script>
import Logo from "assets/bb-emblem.svg"
import { goto } from "@roxi/routify"
import { organisation } from "stores/portal"
</script>
<img src={Logo} alt="Budibase Logo" on:click={() => $goto("./apps")} />
<img
src={$organisation.logoUrl || Logo}
alt="Budibase Logo"
on:click={() => $goto("./apps")}
/>
<style>
img {

View File

@ -6,9 +6,9 @@
import UpgradeButton from "./UpgradeButton.svelte"
import { fade } from "svelte/transition"
import Logo from "./Logo.svelte"
import { menu } from "stores/portal"
export let visible = false
export let menu
const dispatch = createEventDispatcher()
@ -28,17 +28,32 @@
<Logo />
</div>
<SideNav>
{#each menu as { title, href }}
<SideNavItem
text={title}
url={href}
active={$isActive(href)}
on:click={close}
/>
{#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
text={title}
url={href}
active={$isActive(href)}
on:click={close}
/>
{/each}
{/if}
{/each}
</SideNav>
<div>
<UpgradeButton />
<UpgradeButton on:click={close} />
</div>
</Layout>
</div>
@ -47,6 +62,13 @@
.mobile-nav {
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) {
.mobile-nav-underlay {

View File

@ -7,6 +7,7 @@
{#if $admin.cloud && $auth?.user?.accountPortalAccess}
<Button
cta
on:click
on:click={() => {
$goto($admin.accountPortalUrl + "/portal/upgrade")
}}
@ -14,7 +15,12 @@
Upgrade
</Button>
{: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
</Button>
{/if}

View File

@ -23,7 +23,7 @@
<ActionMenu align="right" dataCy="user-menu">
<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" />
</div>
<MenuItem icon="Moon" on:click={() => themeModal.show()} dataCy="theme">

View File

@ -1,9 +1,8 @@
<script>
import { isActive, redirect, goto, url } from "@roxi/routify"
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 { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
import UpgradeButton from "./_components/UpgradeButton.svelte"
import MobileMenu from "./_components/MobileMenu.svelte"
import Logo from "./_components/Logo.svelte"
@ -13,10 +12,9 @@
let mobileMenuVisible = false
let activeTab = "Apps"
$: menu = buildMenu($auth.isAdmin)
$: $url(), updateActiveTab()
$: $url(), updateActiveTab($menu)
const updateActiveTab = () => {
const updateActiveTab = menu => {
for (let entry of menu) {
if ($isActive(entry.href)) {
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 hideMobileMenu = () => (mobileMenuVisible = false)
@ -104,7 +53,7 @@
</div>
<div class="desktop">
<Tabs selected={activeTab}>
{#each menu as { title, href }}
{#each $menu as { title, href }}
<Tab {title} on:click={() => $goto(href)} />
{/each}
</Tabs>
@ -122,7 +71,7 @@
<div class="main">
<slot />
</div>
<MobileMenu visible={mobileMenuVisible} {menu} on:close={hideMobileMenu} />
<MobileMenu visible={mobileMenuVisible} on:close={hideMobileMenu} />
</div>
{/if}

View File

@ -1,40 +1,19 @@
<script>
import { url, isActive } from "@roxi/routify"
import { isActive } from "@roxi/routify"
import { Page } from "@budibase/bbui"
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>
<Page narrow>
<Content>
<div slot="side-nav">
<SideNav>
<!-- Always show usage in self-host or cloud if licensing enabled-->
<SideNavItem
text="Usage"
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}
{#each pages as { title, href }}
<SideNavItem text={title} url={href} active={$isActive(href)} />
{/each}
</SideNav>
</div>
<slot />

View File

@ -32,6 +32,8 @@
$: license = $auth.user?.license
$: accountPortalAccess = $auth?.user?.accountPortalAccess
$: quotaReset = quotaUsage?.quotaReset
$: canManagePlan =
($admin.cloud && accountPortalAccess) || (!$admin.cloud && $auth.isAdmin)
const setMonthlyUsage = () => {
monthlyUsage = []
@ -184,10 +186,15 @@
</Body>
</Layout>
<Divider />
<Body>
To upgrade your plan and usage limits visit your
<Link size="L" on:click={goToAccountPortal}>account</Link>.
</Body>
{#if canManagePlan}
<Body>
To upgrade your plan and usage limits visit your
<Link size="L" on:click={goToAccountPortal}>account</Link>.
</Body>
{:else}
<Body>Contact your account holder to upgrade your plan.</Body>
{/if}
<DashCard
description="YOUR CURRENT PLAN"
title={planTitle()}
@ -242,6 +249,7 @@
justify-content: flex-start;
align-items: flex-start;
gap: 40px;
flex-wrap: wrap;
}
.column {
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;
align-items: center;
gap: var(--spacing-xl);
flex-wrap: wrap;
}
.app-actions :global(.spectrum-Textfield) {
max-width: 180px;
}
.app-table {
@ -380,6 +384,9 @@
margin-top: var(--spacing-xl);
margin-bottom: calc(-1 * var(--spacing-m));
}
.app-actions :global(.spectrum-Textfield) {
max-width: none;
}
/* Hide download apps icon */
.app-actions :global(> .spectrum-Icon) {
display: none;

View File

@ -260,10 +260,16 @@
.desktop {
display: contents;
}
.mobile {
display: none;
}
@media (max-width: 640px) {
.desktop {
display: none;
}
.mobile {
display: contents;
}
}
</style>

View File

@ -14,6 +14,8 @@
export let app
export let appUsers = []
export let showUsers = false
export let showGroups = false
const dispatch = createEventDispatcher()
const usersFetch = fetchData({
@ -41,7 +43,8 @@
$: availableGroups = getAvailableGroups($groups, app.appId, search, data)
$: valid = data?.length && !data?.some(x => !x.id?.length || !x.role?.length)
$: optionSections = {
...($licensing.groupsEnabled &&
...(showGroups &&
$licensing.groupsEnabled &&
availableGroups.length && {
["User groups"]: {
data: availableGroups,
@ -51,13 +54,15 @@
getColour: group => group.color,
},
}),
users: {
data: availableUsers,
getLabel: user => user.email,
getValue: user => user._id,
getIcon: user => user.icon,
getColour: user => user.color,
},
...(showUsers && {
users: {
data: availableUsers,
getLabel: user => user.email,
getValue: user => user._id,
getIcon: user => user.icon,
getColour: user => user.color,
},
}),
}
const addData = async appData => {
@ -139,7 +144,7 @@
<ModalContent
size="M"
title="Assign users to your app"
title="Assign access to your app"
confirmText="Done"
cancelText="Cancel"
onConfirm={() => addData(data)}
@ -185,7 +190,7 @@
</Layout>
{/if}
<div>
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton>
<ActionButton on:click={addNewInput} icon="Add">Add more</ActionButton>
</div>
</ModalContent>

View File

@ -36,7 +36,7 @@
},
role: {
displayName: "Access",
width: "160px",
width: "150px",
borderLeft: true,
},
}
@ -50,6 +50,8 @@
let assignmentModal
let appGroups
let appUsers
let showAddUsers = false
let showAddGroups = false
$: app = $overview.selectedApp
$: devAppId = app.devId
@ -153,6 +155,18 @@
await usersFetch.refresh()
}
const showUsersModal = () => {
showAddUsers = true
showAddGroups = false
assignmentModal.show()
}
const showGroupsModal = () => {
showAddUsers = false
showAddGroups = true
assignmentModal.show()
}
setContext("roles", {
updateRole,
removeRole,
@ -178,7 +192,7 @@
<Layout noPadding gap="S">
<div class="title">
<Heading size="S">Users</Heading>
<Button secondary on:click={assignmentModal.show}>Assign user</Button>
<Button cta on:click={showUsersModal}>Assign user</Button>
</div>
<Table
customPlaceholder
@ -203,13 +217,11 @@
</Layout>
{/if}
{#if $usersFetch.loaded && $licensing.groupsEnabled && appGroups.length}
{#if $usersFetch.loaded && $licensing.groupsEnabled}
<Layout noPadding gap="S">
<div class="title">
<Heading size="S">Groups</Heading>
<Button secondary on:click={assignmentModal.show}>
Assign group
</Button>
<Button cta on:click={showGroupsModal}>Assign group</Button>
</div>
<Table
customPlaceholder
@ -228,7 +240,13 @@
</Layout>
<Modal bind:this={assignmentModal}>
<AssignmentModal {app} {appUsers} on:update={usersFetch.refresh} />
<AssignmentModal
{app}
{appUsers}
on:update={usersFetch.refresh}
showGroups={showAddGroups}
showUsers={showAddUsers}
/>
</Modal>
<style>

View File

@ -1,6 +1,5 @@
<script>
import {
ActionButton,
Button,
DatePicker,
Divider,
@ -90,7 +89,7 @@
},
actions: {
displayName: null,
width: "5%",
width: "auto",
},
}
@ -246,9 +245,7 @@
/>
</div>
<div>
<ActionButton on:click={modal.show} icon="SaveAsFloppy">
Create new backup
</ActionButton>
<Button cta on:click={modal.show}>Create new backup</Button>
</div>
</div>
<div class="table">

View File

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

View File

@ -1,43 +1,20 @@
<script>
import { url, isActive } from "@roxi/routify"
import { isActive } from "@roxi/routify"
import { Page } from "@budibase/bbui"
import { Content, SideNav, SideNavItem } from "components/portal/page"
import { admin } from "stores/portal"
import { menu } from "stores/portal"
$: wide = $isActive("./email/:template")
$: pages = $menu.find(x => x.title === "Settings").subPages
</script>
<Page>
<Content narrow={!wide}>
<div slot="side-nav">
<SideNav>
<SideNavItem
text="Auth"
url={$url("./auth")}
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}
{#each pages as { title, href }}
<SideNavItem text={title} url={href} active={$isActive(href)} />
{/each}
</SideNav>
</div>
<slot />

View File

@ -1,25 +1,20 @@
<script>
import { Page } from "@budibase/bbui"
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")
$: pages = $menu.find(x => x.title === "Users").subPages
</script>
<Page>
<Content narrow={!wide}>
<div slot="side-nav">
<SideNav>
<SideNavItem
text="Users"
url={$url("./users")}
active={$isActive("./users")}
/>
<SideNavItem
text="Groups"
url={$url("./groups")}
active={$isActive("./groups")}
/>
{#each pages as { title, href }}
<SideNavItem text={title} url={href} active={$isActive(href)} />
{/each}
</SideNav>
</div>
<slot />

View File

@ -14,7 +14,7 @@
} from "@budibase/bbui"
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
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 { roles } from "stores/backend"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -70,6 +70,7 @@
let loaded = false
let editModal, deleteModal
$: readonly = !$auth.isAdmin
$: page = $pageInfo.page
$: fetchUsers(page, searchTerm)
$: group = $groups.find(x => x._id === groupId)
@ -84,7 +85,6 @@
...app,
role: group?.roles?.[apps.getProdAppID(app.devId)],
}))
$: console.log(groupApps)
$: {
if (loaded && !group?._id) {
$goto("./")
@ -164,24 +164,28 @@
<div class="header">
<GroupIcon {group} size="L" />
<Heading>{group?.name}</Heading>
<ActionMenu align="right">
<span slot="control">
<Icon hoverable name="More" />
</span>
<MenuItem icon="Refresh" on:click={() => editModal.show()}>
Edit
</MenuItem>
<MenuItem icon="Delete" on:click={() => deleteModal.show()}>
Delete
</MenuItem>
</ActionMenu>
{#if !readonly}
<ActionMenu align="right">
<span slot="control">
<Icon hoverable name="More" />
</span>
<MenuItem icon="Refresh" on:click={() => editModal.show()}>
Edit
</MenuItem>
<MenuItem icon="Delete" on:click={() => deleteModal.show()}>
Delete
</MenuItem>
</ActionMenu>
{/if}
</div>
<Layout noPadding gap="S">
<div class="header">
<Heading size="S">Users</Heading>
<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>
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<UserGroupPicker
@ -246,7 +250,7 @@
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-m);
gap: var(--spacing-l);
}
.header :global(.spectrum-Heading) {
flex: 1 1 auto;

View File

@ -1,6 +1,7 @@
<script>
import { ActionButton } from "@budibase/bbui"
import { getContext } from "svelte"
import { auth } from "stores/portal"
export let value
@ -12,4 +13,6 @@
}
</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 },
]
$: readonly = !$auth.isAdmin
$: schema = {
name: { displayName: "Group", width: "2fr", minWidth: "200px" },
users: { sortable: false, width: "1fr" },
@ -108,7 +109,9 @@
<ButtonGroup>
{#if $licensing.groupsEnabled}
<!--Show the group create button-->
<Button cta on:click={showCreateGroupModal}>Add group</Button>
<Button disabled={readonly} cta on:click={showCreateGroupModal}>
Add group
</Button>
{:else}
<Button
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 loaded = false
$: readonly = !$auth.isAdmin
$: fullName = user?.firstName ? user?.firstName + " " + user?.lastName : ""
$: privileged = user?.admin?.global || user?.builder?.global
$: nameLabel = getNameLabel(user)
@ -235,18 +236,16 @@
</Breadcrumbs>
<div class="title">
<div>
<div style="display: flex;">
<Avatar size="XXL" {initials} />
<div class="subtitle">
<Heading size="M">{nameLabel}</Heading>
{#if nameLabel !== user?.email}
<Body size="S">{user?.email}</Body>
{/if}
</div>
<div class="user-info">
<Avatar size="XXL" {initials} />
<div class="subtitle">
<Heading size="M">{nameLabel}</Heading>
{#if nameLabel !== user?.email}
<Body size="S">{user?.email}</Body>
{/if}
</div>
</div>
{#if userId !== $auth.user?._id}
{#if userId !== $auth.user?._id && !readonly}
<div>
<ActionMenu align="right">
<span slot="control">
@ -271,17 +270,26 @@
</div>
<div class="field">
<Label size="L">First name</Label>
<Input value={user?.firstName} on:blur={updateUserFirstName} />
<Input
disabled={readonly}
value={user?.firstName}
on:blur={updateUserFirstName}
/>
</div>
<div class="field">
<Label size="L">Last name</Label>
<Input value={user?.lastName} on:blur={updateUserLastName} />
<Input
disabled={readonly}
value={user?.lastName}
on:blur={updateUserLastName}
/>
</div>
<!-- don't let a user remove the privileges that let them be here -->
{#if userId !== $auth.user._id}
<div class="field">
<Label size="L">Role</Label>
<Select
disabled={readonly}
value={globalRole}
options={Constants.BudibaseRoleOptions}
on:change={updateUserRole}
@ -297,7 +305,9 @@
<div class="tableTitle">
<Heading size="S">Groups</Heading>
<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>
<Popover align="right" bind:this={popover} anchor={popoverAnchor}>
<UserGroupPicker
@ -375,13 +385,18 @@
align-items: center;
justify-content: space-between;
}
.user-info {
display: flex;
align-items: center;
justify-content: flex-start;
gap: var(--spacing-l);
}
.tableTitle {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.subtitle {
padding: 0 0 0 var(--spacing-m);
display: flex;
flex-direction: column;
justify-content: center;

View File

@ -1,6 +1,7 @@
<script>
import { ActionButton } from "@budibase/bbui"
import { getContext } from "svelte"
import { auth } from "stores/portal"
export let value
@ -12,4 +13,6 @@
}
</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 createUserModal,
inviteConfirmationModal,
@ -52,6 +52,7 @@
]
let userData = []
$: readonly = !$auth.isAdmin
$: debouncedUpdateFetch(searchEmail)
$: schema = {
email: {
@ -205,62 +206,67 @@
onMount(async () => {
try {
loaded = false
await groups.actions.init()
loaded = true
groupsLoaded = true
} catch (error) {
notifications.error("Error fetching User Group data")
notifications.error("Error fetching user group data")
}
})
</script>
{#if loaded && $fetch.loaded}
<Layout noPadding gap="M">
<Layout gap="XS" noPadding>
<Heading>Users</Heading>
<Body>Add users and control who gets access to your published apps</Body>
</Layout>
<Divider />
<div class="controls">
<ButtonGroup>
<Button dataCy="add-user" on:click={createUserModal.show} cta>
Add users
</Button>
<Button on:click={importUsersModal.show} secondary>Import</Button>
</ButtonGroup>
<div class="controls-right">
<Search bind:value={searchEmail} placeholder="Search" />
{#if selectedRows.length > 0}
<DeleteRowsButton
item="user"
on:updaterows
{selectedRows}
{deleteRows}
/>
{/if}
</div>
</div>
<Table
on:click={({ detail }) => $goto(`./${detail._id}`)}
{schema}
bind:selectedRows
data={enrichedUsers}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={true}
{customRenderers}
/>
<div class="pagination">
<Pagination
page={$fetch.pageNumber + 1}
hasPrevPage={$fetch.loading ? false : $fetch.hasPrevPage}
hasNextPage={$fetch.loading ? false : $fetch.hasNextPage}
goToPrevPage={fetch.prevPage}
goToNextPage={fetch.nextPage}
/>
</div>
<Layout noPadding gap="M">
<Layout gap="XS" noPadding>
<Heading>Users</Heading>
<Body>Add users and control who gets access to your published apps</Body>
</Layout>
{/if}
<Divider />
<div class="controls">
<ButtonGroup>
<Button
disabled={readonly}
dataCy="add-user"
on:click={createUserModal.show}
cta
>
Add users
</Button>
<Button disabled={readonly} on:click={importUsersModal.show} secondary>
Import
</Button>
</ButtonGroup>
<div class="controls-right">
<Search bind:value={searchEmail} placeholder="Search" />
{#if selectedRows.length > 0}
<DeleteRowsButton
item="user"
on:updaterows
{selectedRows}
{deleteRows}
/>
{/if}
</div>
</div>
<Table
on:click={({ detail }) => $goto(`./${detail._id}`)}
{schema}
bind:selectedRows
data={enrichedUsers}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={!readonly}
{customRenderers}
loading={!$fetch.loaded || !groupsLoaded}
/>
<div class="pagination">
<Pagination
page={$fetch.pageNumber + 1}
hasPrevPage={$fetch.loading ? false : $fetch.hasPrevPage}
hasNextPage={$fetch.loading ? false : $fetch.hasNextPage}
goToPrevPage={fetch.prevPage}
goToNextPage={fetch.nextPage}
/>
</div>
</Layout>
<Modal bind:this={createUserModal}>
<AddUserModal {showOnboardingTypeModal} />

View File

@ -12,3 +12,4 @@ export { plugins } from "./plugins"
export { backups } from "./backups"
export { overview } from "./overview"
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
})