add plugins ui
This commit is contained in:
parent
f14af4bae9
commit
accdfd9b9e
|
@ -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}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 { templates } from "./templates"
|
||||||
export { licensing } from "./licensing"
|
export { licensing } from "./licensing"
|
||||||
export { groups } from "./groups"
|
export { groups } from "./groups"
|
||||||
|
export { plugins } from "./plugins"
|
|
@ -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
|
* 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}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue