add plugins ui
This commit is contained in:
parent
bec08eecc8
commit
b9b8d59005
10 changed files with 377 additions and 10 deletions
|
@ -24,7 +24,7 @@
|
|||
export let secondaryAction = undefined
|
||||
export let secondaryButtonWarning = false
|
||||
export let dataCy = null
|
||||
|
||||
export let buttonCta = true
|
||||
const { hide, cancel } = getContext(Context.Modal)
|
||||
let loading = false
|
||||
$: confirmDisabled = disabled || loading
|
||||
|
@ -93,7 +93,6 @@
|
|||
class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter"
|
||||
>
|
||||
<slot name="footer" />
|
||||
|
||||
{#if showSecondaryButton && secondaryButtonText && secondaryAction}
|
||||
<div class="secondary-action">
|
||||
<Button
|
||||
|
@ -114,7 +113,7 @@
|
|||
<span class="confirm-wrap">
|
||||
<Button
|
||||
group
|
||||
cta
|
||||
cta={buttonCta}
|
||||
{...$$restProps}
|
||||
disabled={confirmDisabled}
|
||||
on:click={confirm}
|
||||
|
|
|
@ -55,6 +55,8 @@
|
|||
},
|
||||
{ title: "Auth", href: "/builder/portal/manage/auth" },
|
||||
{ title: "Email", href: "/builder/portal/manage/email" },
|
||||
{ title: "Plugins", href: "/builder/portal/manage/plugins" },
|
||||
|
||||
{
|
||||
title: "Organisation",
|
||||
href: "/builder/portal/settings/organisation",
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -8,3 +8,4 @@ export { oidc } from "./oidc"
|
|||
export { templates } from "./templates"
|
||||
export { licensing } from "./licensing"
|
||||
export { groups } from "./groups"
|
||||
export { plugins } from "./plugins"
|
47
packages/builder/src/stores/portal/plugins.js
Normal file
47
packages/builder/src/stores/portal/plugins.js
Normal 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()
|
|
@ -3,9 +3,9 @@ export const buildPluginEndpoints = API => ({
|
|||
* Uploads a plugin tarball bundle
|
||||
* @param data the plugin tarball bundle to upload
|
||||
*/
|
||||
uploadPlugin: async data => {
|
||||
uploadPlugin: async (data, source) => {
|
||||
return await API.post({
|
||||
url: "/api/plugin/upload",
|
||||
url: `/api/plugin/upload/${source}`,
|
||||
body: data,
|
||||
json: false,
|
||||
})
|
||||
|
@ -19,4 +19,17 @@ export const buildPluginEndpoints = API => ({
|
|||
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}`,
|
||||
})
|
||||
},
|
||||
|
||||
})
|
||||
|
|
|
@ -21,6 +21,7 @@ export async function getPlugins(type?: PluginType) {
|
|||
}
|
||||
|
||||
export async function upload(ctx: any) {
|
||||
const { source } = ctx.params
|
||||
const plugins: FileType[] =
|
||||
ctx.request.files.file.length > 1
|
||||
? Array.from(ctx.request.files.file)
|
||||
|
@ -29,7 +30,7 @@ export async function upload(ctx: any) {
|
|||
let docs = []
|
||||
// can do single or multiple plugins
|
||||
for (let plugin of plugins) {
|
||||
const doc = await processPlugin(plugin)
|
||||
const doc = await processPlugin(plugin, source)
|
||||
docs.push(doc)
|
||||
}
|
||||
ctx.body = {
|
||||
|
@ -46,9 +47,15 @@ export async function fetch(ctx: any) {
|
|||
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 { metadata, directory } = await extractPluginTarball(plugin)
|
||||
const version = metadata.package.version,
|
||||
|
@ -80,6 +87,7 @@ export async function processPlugin(plugin: FileType) {
|
|||
const doc = {
|
||||
_id: pluginId,
|
||||
_rev: rev,
|
||||
source,
|
||||
name,
|
||||
version,
|
||||
description,
|
||||
|
|
|
@ -6,8 +6,8 @@ import { BUILDER } from "@budibase/backend-core/permissions"
|
|||
const router = new 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)
|
||||
.delete("/api/plugin/:pluginId", authorized(BUILDER), controller.destroy)
|
||||
.delete("/api/plugin/:pluginId/:pluginRev", authorized(BUILDER), controller.destroy)
|
||||
|
||||
export default router
|
||||
|
|
Loading…
Reference in a new issue