Merge branch 'master' of github.com:Budibase/budibase into feature/page-refactor

This commit is contained in:
mike12345567 2020-11-30 11:47:08 +00:00
commit b55c4bc9c0
23 changed files with 156 additions and 213 deletions

View File

@ -60,7 +60,7 @@ context("Create a Table", () => {
})
it("deletes a table", () => {
cy.contains(".nav-item", "dog").get(".actions").invoke("show").click()
cy.get(".actions").first().invoke("show").click()
cy.get("[data-cy=delete-table]").click()
cy.contains("Delete Table").click()
cy.contains("dog").should("not.exist")

View File

@ -9,9 +9,9 @@ context('Create a User', () => {
// https://on.cypress.io/interacting-with-elements
it('should create a user', () => {
cy.createUser('bbuser', 'test', 'POWER_USER')
cy.createUser("bbuser", "test", "ADMIN")
// Check to make sure user was created!
cy.get("input[disabled]").should('have.value', 'bbuser')
// // Check to make sure user was created!
cy.contains("bbuser").should('be.visible')
})
})

View File

@ -113,23 +113,26 @@ Cypress.Commands.add("addRow", values => {
Cypress.Commands.add("createUser", (username, password, accessLevel) => {
// Create User
cy.get(".toprightnav > .settings").click()
cy.contains("Users").click()
cy.get("[name=Name]")
.first()
.type(username)
cy.get("[name=Password]")
cy.contains("Create New Row").click()
cy.get(".modal").within(() => {
cy.get("input")
.first()
.type(password)
cy.get("input")
.eq(1)
.type(username)
cy.get("select")
.first()
.select(accessLevel)
// Save
cy.get(".inputs")
.contains("Create")
cy.get(".buttons")
.contains("Create Row")
.click()
})
})
Cypress.Commands.add("addHeadlineComponent", text => {

View File

@ -1,14 +1,19 @@
<script>
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../helpers"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
export let meta
export let creating
export let value = meta.type === "boolean" ? false : ""
$: type = meta.type
$: label = capitalise(meta.name)
$: editingUser =
!creating && $backendUiStore.selectedTable?._id === TableNames.USERS
</script>
{#if type === 'options'}
@ -30,5 +35,11 @@
{:else if type === 'link'}
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
{:else}
<Input thin {label} data-cy="{meta.name}-input" {type} bind:value />
<Input
thin
{label}
data-cy="{meta.name}-input"
{type}
bind:value
disabled={editingUser} />
{/if}

View File

@ -7,8 +7,8 @@ export async function createUser(user) {
}
export async function saveRow(row, tableId) {
const SAVE_ROWS_URL = `/api/${tableId}/rows`
const response = await api.post(SAVE_ROWS_URL, row)
const SAVE_ROW_URL = `/api/${tableId}/rows`
const response = await api.post(SAVE_ROW_URL, row)
return await response.json()
}

View File

@ -2,6 +2,7 @@
import { Input, Button, TextButton, Select, Toggle } from "@budibase/bbui"
import { cloneDeep } from "lodash/fp"
import { backendUiStore } from "builderStore"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
import { FIELDS } from "constants/backend"
import { notifier } from "builderStore/store/notifications"
import ValuesList from "components/common/ValuesList.svelte"
@ -30,6 +31,9 @@
table => table._id !== $backendUiStore.draftTable._id
)
$: required = !!field?.constraints?.presence || primaryDisplay
$: uneditable =
$backendUiStore.selectedTable?._id === TableNames.USERS &&
UNEDITABLE_USER_FIELDS.includes(field.name)
async function saveColumn() {
backendUiStore.update(state => {
@ -87,7 +91,7 @@
</script>
<div class="actions" class:hidden={deletion}>
<Input label="Name" thin bind:value={field.name} />
<Input label="Name" thin bind:value={field.name} disabled={uneditable} />
<Select
disabled={originalName}
@ -101,7 +105,7 @@
{/each}
</Select>
{#if field.type !== 'link'}
{#if field.type !== 'link' && !uneditable}
<Toggle
checked={required}
on:change={onChangeRequired}
@ -157,7 +161,7 @@
bind:value={field.fieldName} />
{/if}
<footer class="create-column-options">
{#if originalName}
{#if !uneditable && originalName}
<TextButton text on:click={confirmDelete}>Delete Column</TextButton>
{/if}
<Button secondary on:click={onClosed}>Cancel</Button>

View File

@ -1,5 +1,6 @@
<script>
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import { notifier } from "builderStore/store/notifications"
import RowFieldControl from "../RowFieldControl.svelte"
import * as api from "../api"
@ -21,9 +22,10 @@
{ ...row, tableId: table._id },
table._id
)
if (rowResponse.errors) {
errors = Object.keys(rowResponse.errors)
.map(k => ({ dataPath: k, message: rowResponse.errors[k] }))
errors = Object.entries(rowResponse.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
// Prevent modal closing if there were errors
return false
@ -38,9 +40,15 @@
confirmText={creating ? 'Create Row' : 'Save Row'}
onConfirm={saveRow}>
<ErrorsBox {errors} />
{#if creating && table._id === TableNames.USERS}
<RowFieldControl
{creating}
meta={{ name: 'password', type: 'password' }}
bind:value={row.password} />
{/if}
{#each tableSchema as [key, meta]}
<div>
<RowFieldControl {meta} bind:value={row[key]} />
<RowFieldControl {meta} bind:value={row[key]} {creating} />
</div>
{/each}
</ModalContent>

View File

@ -1,6 +1,7 @@
<script>
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import ListItem from "./ListItem.svelte"
import CreateTableModal from "./modals/CreateTableModal.svelte"
import EditTablePopover from "./popovers/EditTablePopover.svelte"
@ -42,7 +43,7 @@
{#each $backendUiStore.tables as table, idx}
<NavItem
border={idx > 0}
icon="ri-table-line"
icon={`ri-${table._id === TableNames.USERS ? 'user' : 'table'}-line`}
text={table.name}
selected={selectedView === `all_${table._id}`}
on:click={() => selectTable(table)}>

View File

@ -39,7 +39,7 @@
async function deleteTable() {
await backendUiStore.actions.tables.delete(table)
store.store.actions.screens.delete(templateScreens)
store.actions.screens.delete(templateScreens)
await backendUiStore.actions.tables.fetch()
notifier.success("Table deleted")
hideEditor()

View File

@ -1,5 +1,5 @@
<script>
import { General, Users, DangerZone, APIKeys } from "./tabs"
import { General, DangerZone, APIKeys } from "./tabs"
import { Switcher, ModalContent } from "@budibase/bbui"
const tabs = [
@ -8,11 +8,6 @@
key: "GENERAL",
component: General,
},
{
title: "Users",
key: "USERS",
component: Users,
},
{
title: "API Keys",
key: "API_KEYS",

View File

@ -1,43 +0,0 @@
<script>
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
import { Input, Select, Button } from "@budibase/bbui"
export let user
let editMode = false
</script>
<div class="inputs">
<Input
disabled
thin
bind:value={user.username}
name="Name"
placeholder="Username" />
<Select disabled={!editMode} bind:value={user.accessLevelId} thin secondary>
<option value="">Choose an option</option>
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
</Select>
{#if editMode}
<Button
blue
on:click={() => {
dispatch('save', user)
editMode = false
}}>
Save
</Button>
{:else}
<Button secondary on:click={() => (editMode = true)}>Edit</Button>
{/if}
</div>
<style>
.inputs {
display: grid;
justify-items: stretch;
grid-gap: var(--spacing-m);
grid-template-columns: 1fr 1fr 140px;
}
</style>

View File

@ -1,114 +0,0 @@
<script>
import { Input, Select, Button, Label } from "@budibase/bbui"
import UserRow from "../UserRow.svelte"
import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
// import * as api from "../api"
let username = ""
let password = ""
let accessLevelId = "ADMIN"
$: valid = username && password && accessLevelId
$: appId = $store.appId
// Create user!
async function createUser() {
if (valid) {
const user = { name: username, username, password, accessLevelId }
const response = await api.post(`/api/users`, user)
const json = await response.json()
backendUiStore.actions.users.create(json)
fetchUsersPromise = fetchUsers()
}
}
// Update user!
async function updateUser(event) {
let data = event.detail
delete data.password
const response = await api.put(`/api/users`, data)
const users = await response.json()
backendUiStore.update(state => {
state.users = users
return state
})
fetchUsersPromise = fetchUsers()
}
// Get users
async function fetchUsers() {
const response = await api.get(`/api/users`)
const users = await response.json()
backendUiStore.update(state => {
state.users = users
return state
})
return users
}
let fetchUsersPromise = fetchUsers()
</script>
<div class="container">
<div>
<Label extraSmall grey>Create New User</Label>
<div class="inputs">
<Input thin bind:value={username} name="Name" placeholder="Username" />
<Input
thin
type="password"
bind:value={password}
name="Password"
placeholder="Password" />
<Select secondary bind:value={accessLevelId} thin>
<option value="">Choose an option</option>
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
</Select>
<Button on:click={createUser} primary>Create</Button>
</div>
</div>
<div>
<Label extraSmall grey>Current Users</Label>
{#await fetchUsersPromise}
Loading...
{:then users}
<ul>
{#each users as user}
<li>
<UserRow {user} on:save={updateUser} />
</li>
{:else}
<li>No Users found</li>
{/each}
</ul>
{:catch err}
Something went wrong when trying to fetch users. Please refresh (CMD + R /
CTRL + R) the page and try again.
{/await}
</div>
</div>
<style>
.container {
display: grid;
grid-gap: var(--spacing-xl);
}
.inputs {
display: grid;
justify-items: stretch;
grid-gap: var(--spacing-m);
grid-template-columns: 1fr 1fr 1fr 140px;
}
ul {
list-style: none;
padding: 0;
display: grid;
grid-gap: var(--spacing-m);
margin: 0;
}
</style>

View File

@ -1,6 +1,5 @@
export { default as General } from "./General.svelte"
export { default as Integrations } from "./Integrations.svelte"
export { default as Permissions } from "./Permissions.svelte"
export { default as Users } from "./Users.svelte"
export { default as APIKeys } from "./APIKeys.svelte"
export { default as DangerZone } from "./DangerZone.svelte"

View File

@ -1,3 +1,10 @@
export const TableNames = {
USERS: "ta_users",
}
// fields on the user table that cannot be edited
export const UNEDITABLE_USER_FIELDS = ["username", "password", "accessLevelId"]
export const DEFAULT_LAYOUTS = {
main: {
props: {

View File

@ -68,16 +68,11 @@
<div class="toprightnav">
<ThemeEditor />
<FeedbackNavLink />
<div class="topnavitemright">
<a target="_blank" href="https://docs.budibase.com">
<i class="ri-question-line" />
</a>
</div>
<div class="topnavitemright">
<a
target="_blank"
href="https://github.com/Budibase/budibase/discussions">
<i class="ri-discuss-line" />
<i class="ri-question-line" />
</a>
</div>
<SettingsLink />

View File

@ -81,6 +81,7 @@
"lodash": "^4.17.13",
"mustache": "^4.0.1",
"node-fetch": "^2.6.0",
"open": "^7.3.0",
"pino-pretty": "^4.0.0",
"pouchdb": "^7.2.1",
"pouchdb-all-dbs": "^1.0.2",

View File

@ -31,6 +31,7 @@ const { HOME_SCREEN } = require("../../constants/screens")
const { cloneDeep } = require("lodash/fp")
const { recurseMustache } = require("../../utilities/mustache")
const { generateAssetCss } = require("../../utilities/builder/generateCss")
const { USERS_TABLE_SCHEMA } = require("../../constants")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
@ -85,6 +86,9 @@ async function createInstance(template) {
if (!ok) {
throw "Error loading database dump from template."
}
} else {
// create the users table
await db.put(USERS_TABLE_SCHEMA)
}
return { _id: appId }

View File

@ -6,7 +6,9 @@ const {
generateRowID,
DocumentTypes,
SEPARATOR,
ViewNames,
} = require("../../db/utils")
const usersController = require("./user")
const { cloneDeep } = require("lodash")
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
@ -118,6 +120,16 @@ exports.save = async function(ctx) {
table,
})
// Creation of a new user goes to the user controller
if (!existingRow && row.tableId === ViewNames.USERS) {
try {
await usersController.create(ctx)
} catch (err) {
ctx.body = { errors: [err.message] }
}
return
}
if (existingRow) {
const response = await db.put(row)
row._rev = response.rev
@ -315,8 +327,8 @@ exports.fetchEnrichedRow = async function(ctx) {
ctx.status = 200
}
function coerceRowValues(rec, table) {
const row = cloneDeep(rec)
function coerceRowValues(record, table) {
const row = cloneDeep(record)
for (let [key, value] of Object.entries(row)) {
const field = table.schema[key]
if (!field) continue

View File

@ -1,6 +1,6 @@
const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt")
const { generateUserID, getUserParams } = require("../../db/utils")
const { generateUserID, getUserParams, ViewNames } = require("../../db/utils")
const {
BUILTIN_LEVEL_ID_ARRAY,
} = require("../../utilities/security/accessLevels")
@ -11,7 +11,7 @@ const {
exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
const data = await database.allDocs(
getUserParams(null, {
getUserParams("", {
include_docs: true,
})
)
@ -44,6 +44,7 @@ exports.create = async function(ctx) {
type: "user",
accessLevelId,
permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER],
tableId: ViewNames.USERS,
}
try {

View File

@ -28,7 +28,7 @@ module.exports.definition = {
accessLevelId: {
type: "string",
title: "Access Level",
enum: accessLevels.BUILTIN_LEVEL_IDS,
enum: accessLevels.BUILTIN_LEVEL_ID_ARRAY,
pretty: accessLevels.BUILTIN_LEVEL_NAME_ARRAY,
},
},

View File

@ -1,7 +1,42 @@
const { BUILTIN_LEVEL_IDS } = require("../utilities/security/accessLevels")
const AuthTypes = {
APP: "app",
BUILDER: "builder",
EXTERNAL: "external",
}
const USERS_TABLE_SCHEMA = {
_id: "ta_users",
type: "table",
views: {},
name: "Users",
schema: {
username: {
type: "string",
constraints: {
type: "string",
length: {
maximum: "",
},
presence: true,
},
fieldName: "username",
name: "username",
},
accessLevelId: {
fieldName: "accessLevelId",
name: "accessLevelId",
type: "options",
constraints: {
type: "string",
presence: false,
inclusion: Object.keys(BUILTIN_LEVEL_IDS),
},
},
},
primaryDisplay: "username",
}
exports.AuthTypes = AuthTypes
exports.USERS_TABLE_SCHEMA = USERS_TABLE_SCHEMA

View File

@ -20,6 +20,7 @@ const DocumentTypes = {
const ViewNames = {
LINK: "by_link",
ROUTING: "screen_routes",
USERS: "ta_users",
}
exports.ViewNames = ViewNames
@ -80,13 +81,12 @@ exports.generateTableID = () => {
exports.getRowParams = (tableId = null, rowId = null, otherProps = {}) => {
if (tableId == null) {
return getDocParams(DocumentTypes.ROW, null, otherProps)
} else {
const endOfKey =
rowId == null
? `${tableId}${SEPARATOR}`
: `${tableId}${SEPARATOR}${rowId}`
return getDocParams(DocumentTypes.ROW, endOfKey, otherProps)
}
const endOfKey =
rowId == null ? `${tableId}${SEPARATOR}` : `${tableId}${SEPARATOR}${rowId}`
return getDocParams(DocumentTypes.ROW, endOfKey, otherProps)
}
/**
@ -101,8 +101,12 @@ exports.generateRowID = tableId => {
/**
* Gets parameters for retrieving users, this is a utility function for the getDocParams function.
*/
exports.getUserParams = (username = null, otherProps = {}) => {
return getDocParams(DocumentTypes.USER, username, otherProps)
exports.getUserParams = (username = "", otherProps = {}) => {
return getDocParams(
DocumentTypes.ROW,
`${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${username}`,
otherProps
)
}
/**
@ -111,7 +115,7 @@ exports.getUserParams = (username = null, otherProps = {}) => {
* @returns {string} The new user ID which the user doc can be stored under.
*/
exports.generateUserID = username => {
return `${DocumentTypes.USER}${SEPARATOR}${username}`
return `${DocumentTypes.ROW}${SEPARATOR}${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${username}`
}
/**

View File

@ -3868,6 +3868,11 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2:
is-data-descriptor "^1.0.0"
kind-of "^6.0.2"
is-docker@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156"
integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==
is-extendable@^0.1.0, is-extendable@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
@ -4027,6 +4032,13 @@ is-wsl@^1.1.0:
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
is-wsl@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
dependencies:
is-docker "^2.0.0"
is-yarn-global@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
@ -5648,6 +5660,14 @@ only@~0.0.2:
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=
open@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/open/-/open-7.3.0.tgz#45461fdee46444f3645b6e14eb3ca94b82e1be69"
integrity sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw==
dependencies:
is-docker "^2.0.0"
is-wsl "^2.1.1"
optionator@^0.8.1, optionator@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"