add plugins ui

This commit is contained in:
Peter Clement 2022-08-30 10:49:19 +01:00
parent f14af4bae9
commit accdfd9b9e
10 changed files with 377 additions and 10 deletions

View File

@ -24,7 +24,7 @@
export let secondaryAction = undefined export let secondaryAction = undefined
export let secondaryButtonWarning = false export let secondaryButtonWarning = false
export let dataCy = null export let dataCy = null
export let buttonCta = true
const { hide, cancel } = getContext(Context.Modal) const { hide, cancel } = getContext(Context.Modal)
let loading = false let loading = false
$: confirmDisabled = disabled || loading $: confirmDisabled = disabled || loading
@ -93,7 +93,6 @@
class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter" class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter"
> >
<slot name="footer" /> <slot name="footer" />
{#if showSecondaryButton && secondaryButtonText && secondaryAction} {#if showSecondaryButton && secondaryButtonText && secondaryAction}
<div class="secondary-action"> <div class="secondary-action">
<Button <Button
@ -114,7 +113,7 @@
<span class="confirm-wrap"> <span class="confirm-wrap">
<Button <Button
group group
cta cta={buttonCta}
{...$$restProps} {...$$restProps}
disabled={confirmDisabled} disabled={confirmDisabled}
on:click={confirm} on:click={confirm}

View File

@ -55,6 +55,8 @@
}, },
{ title: "Auth", href: "/builder/portal/manage/auth" }, { title: "Auth", href: "/builder/portal/manage/auth" },
{ title: "Email", href: "/builder/portal/manage/email" }, { title: "Email", href: "/builder/portal/manage/email" },
{ title: "Plugins", href: "/builder/portal/manage/plugins" },
{ {
title: "Organisation", title: "Organisation",
href: "/builder/portal/settings/organisation", href: "/builder/portal/settings/organisation",

View File

@ -0,0 +1,122 @@
<script>
import {
ModalContent,
Label,
Input,
Select,
Body,
StatusLight,
Dropzone,
} from "@budibase/bbui"
import { plugins } from "stores/portal"
let authOptions = {
NPM: ["NPM Token", "URL"],
Github: ["Github Token", "URL"],
URL: ["Header", "URL"],
File: ["Path", "Header"],
Upload: ["Upload"],
}
let file
let sourceValue = "Upload"
let verificationSuccessful = false
async function save() {
if (file) {
await plugins.uploadPlugin(file, sourceValue)
}
}
function verify() {
verificationSuccessful = true
// return false so modal doesn't exit
return false
}
</script>
<ModalContent
confirmText={verificationSuccessful ? "Save" : "Verify"}
buttonCta={verificationSuccessful}
onConfirm={verificationSuccessful ? save : verify}
size="M"
title="Add new plugin"
>
<div class="form-row">
<Label size="M">Type</Label>
<Select
value="Datasource"
placeholder={null}
options={["Component", "Datasource"]}
/>
</div>
<div class="form-row">
<Label size="M">Source</Label>
<Select
placeholder={null}
bind:value={sourceValue}
options={["NPM", "Github", "URL", "File", "Upload"]}
/>
</div>
<div class="form-row">
<Label size="M">Name</Label>
<Input />
</div>
{#each authOptions[sourceValue] as option}
{#if option === "Upload"}
<div class="form-row">
<Label size="M">{option}</Label>
<Dropzone
value={[file]}
on:change={e => {
if (!e.detail || e.detail.length === 0) {
file = null
} else {
file = e.detail[0]
}
}}
/>
</div>
{:else}
<div class="form-row">
<Label size="M">{option}</Label>
<Input />
</div>
{/if}
{/each}
<div class="verification" slot="footer">
<div>
<StatusLight
positive={verificationSuccessful}
neutral={!verificationSuccessful}
/>
</div>
<div class="verification-spacing">
{#if verificationSuccessful}
<Body size="XS">Verification Successful</Body>
{:else}
<Body size="XS"><i>Verify your source</i></Body>
{/if}
</div>
</div>
</ModalContent>
<style>
.verification-spacing {
padding-left: var(--spacing-s);
}
.verification {
display: flex;
margin-right: auto;
align-items: center;
}
.form-row {
display: grid;
grid-template-columns: 60px 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View File

@ -0,0 +1,81 @@
<script>
import { Icon, Body, ActionMenu, MenuItem, Detail } from "@budibase/bbui"
import { plugins } from "stores/portal"
export let plugin
$: console.log(plugin)
let modal
function editGroup() {
modal.show()
}
</script>
<div class="row">
<div class="title">
<div class="name">
<div>
<Icon size="M" name={plugin.schema.schema.icon} />
</div>
<div>
<Body
size="S"
color="var(--spectrum-global-color-gray-900)"
weight="800"
>
{plugin.name}
</Body>
<Detail size="S">
<div class="opacity">{plugin.schema.type}</div>
</Detail>
</div>
</div>
</div>
<div class="desktop">{plugin.source || "-"}</div>
<div class="desktop">{plugin.author || "-"}</div>
<div class="desktop">{plugin.version}</div>
<div>
<div>
<ActionMenu align="right">
<span slot="control">
<Icon hoverable name="More" />
</span>
<MenuItem
on:click={() => plugins.deletePlugin(plugin._id, plugin._rev)}
icon="Delete">Delete</MenuItem
>
<MenuItem on:click={() => editGroup(plugin)} icon="Edit">Edit</MenuItem>
</ActionMenu>
</div>
</div>
</div>
<style>
.row {
display: grid;
grid-template-columns: 0.3fr auto auto auto auto;
align-items: center;
background: var(--background);
height: 50px;
padding-left: 20px;
padding-right: 20px;
justify-content: space-between;
}
.name {
grid-gap: var(--spacing-m);
grid-template-columns: 75px 75px;
align-items: center;
display: flex;
}
.opacity {
opacity: 50%;
}
@media (max-width: 640px) {
.desktop {
display: none !important;
}
}
</style>

View File

@ -0,0 +1,94 @@
<script>
import {
Layout,
Heading,
Body,
Button,
Select,
Divider,
Modal,
Search,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { plugins } from "stores/portal"
import PluginRow from "./_components/PluginRow.svelte"
import AddPluginModal from "./_components/AddPluginModal.svelte"
let modal
let searchTerm = ""
let filterOptions = [
{ label: "All Plugins", value: "all" },
{ label: "Components", value: "datasource" },
{ label: "Datasources", value: "component" },
]
let filter = "all"
$: filteredPlugins =
filter === "all" && searchTerm.length === 0
? $plugins
: $plugins
.filter(plugin => plugin.schema.type !== filter)
.filter(plugin =>
plugin?.name?.toLowerCase().includes(searchTerm.toLowerCase())
)
onMount(async () => {
await plugins.load()
})
</script>
<Layout noPadding>
<Layout gap="XS" noPadding>
<div style="display: flex;">
<Heading size="M">Plugins</Heading>
</div>
<Body>Add your own custom datasources and components</Body>
<Divider />
</Layout>
<Layout noPadding>
<div class="align-buttons">
<div>
<Button on:click={modal.show} newStyles cta icon={"Add"}
>Add plugin</Button
>
</div>
<div class="filters">
<div class="select">
<Select
bind:value={filter}
placeholder={null}
options={filterOptions}
/>
</div>
<Search bind:value={searchTerm} />
</div>
</div>
{#if $plugins}
{#each filteredPlugins as plugin}
<PluginRow {plugin} />
{/each}
{/if}
</Layout>
</Layout>
<Modal bind:this={modal}>
<AddPluginModal />
</Modal>
<style>
.filters {
display: flex;
gap: var(--spacing-xl);
}
.align-buttons {
margin-top: -10px;
display: flex;
column-gap: var(--spacing-xl);
justify-content: space-between;
}
.select {
flex-basis: 180px;
}
</style>

View File

@ -8,3 +8,4 @@ export { oidc } from "./oidc"
export { templates } from "./templates" export { templates } from "./templates"
export { licensing } from "./licensing" export { licensing } from "./licensing"
export { groups } from "./groups" export { groups } from "./groups"
export { plugins } from "./plugins"

View File

@ -0,0 +1,47 @@
import { writable } from "svelte/store"
import { API } from "api"
export function createPluginsStore() {
const { subscribe, set, update } = writable([])
async function load() {
const plugins = await API.getPlugins()
set(plugins)
}
async function deletePlugin(pluginId, pluginRev) {
await API.deletePlugin(pluginId, pluginRev)
update(state => {
state = state.filter(
existing => existing._id !== pluginId
)
return state
})
}
async function uploadPlugin(file, source) {
let data = new FormData()
data.append("file", file)
let resp = await API.uploadPlugin(data, source)
let newPlugin = resp.plugins[0]
update(state => {
const currentIdx = state.findIndex(plugin => plugin._id === newPlugin._id)
if (currentIdx >= 0) {
state.splice(currentIdx, 1, newPlugin)
} else {
state.push(newPlugin)
}
return state
})
}
return {
subscribe,
load,
deletePlugin,
uploadPlugin
}
}
export const plugins = createPluginsStore()

View File

@ -3,9 +3,9 @@ export const buildPluginEndpoints = API => ({
* Uploads a plugin tarball bundle * Uploads a plugin tarball bundle
* @param data the plugin tarball bundle to upload * @param data the plugin tarball bundle to upload
*/ */
uploadPlugin: async data => { uploadPlugin: async (data, source) => {
return await API.post({ return await API.post({
url: "/api/plugin/upload", url: `/api/plugin/upload/${source}`,
body: data, body: data,
json: false, json: false,
}) })
@ -19,4 +19,17 @@ export const buildPluginEndpoints = API => ({
url: "/api/plugin", url: "/api/plugin",
}) })
}, },
/**
* Deletes a plugin.
* @param pluginId the ID of the plugin to delete
*
* * @param pluginId the revision of the plugin to delete
*/
deletePlugin: async (pluginId, pluginRev) => {
return await API.delete({
url: `/api/plugin/${pluginId}/${pluginRev}`,
})
},
}) })

View File

@ -21,6 +21,7 @@ export async function getPlugins(type?: PluginType) {
} }
export async function upload(ctx: any) { export async function upload(ctx: any) {
const { source } = ctx.params
const plugins: FileType[] = const plugins: FileType[] =
ctx.request.files.file.length > 1 ctx.request.files.file.length > 1
? Array.from(ctx.request.files.file) ? Array.from(ctx.request.files.file)
@ -29,7 +30,7 @@ export async function upload(ctx: any) {
let docs = [] let docs = []
// can do single or multiple plugins // can do single or multiple plugins
for (let plugin of plugins) { for (let plugin of plugins) {
const doc = await processPlugin(plugin) const doc = await processPlugin(plugin, source)
docs.push(doc) docs.push(doc)
} }
ctx.body = { ctx.body = {
@ -46,9 +47,15 @@ export async function fetch(ctx: any) {
ctx.body = await getPlugins() ctx.body = await getPlugins()
} }
export async function destroy(ctx: any) {} export async function destroy(ctx: any) {
const db = getGlobalDB()
await db.remove(ctx.params.pluginId, ctx.params.pluginRev)
ctx.message = `Plugin ${ctx.params.pluginId} deleted.`
ctx.status = 200
export async function processPlugin(plugin: FileType) { }
export async function processPlugin(plugin: FileType, source?: string) {
const db = getGlobalDB() const db = getGlobalDB()
const { metadata, directory } = await extractPluginTarball(plugin) const { metadata, directory } = await extractPluginTarball(plugin)
const version = metadata.package.version, const version = metadata.package.version,
@ -80,6 +87,7 @@ export async function processPlugin(plugin: FileType) {
const doc = { const doc = {
_id: pluginId, _id: pluginId,
_rev: rev, _rev: rev,
source,
name, name,
version, version,
description, description,

View File

@ -6,8 +6,8 @@ import { BUILDER } from "@budibase/backend-core/permissions"
const router = new Router() const router = new Router()
router router
.post("/api/plugin/upload", authorized(BUILDER), controller.upload) .post("/api/plugin/upload/:source", authorized(BUILDER), controller.upload)
.get("/api/plugin", authorized(BUILDER), controller.fetch) .get("/api/plugin", authorized(BUILDER), controller.fetch)
.delete("/api/plugin/:pluginId", authorized(BUILDER), controller.destroy) .delete("/api/plugin/:pluginId/:pluginRev", authorized(BUILDER), controller.destroy)
export default router export default router