dashboard-v2/frontend/src/components/IntegrationEditor.svelte
2022-07-21 22:52:58 +01:00

519 lines
18 KiB
Svelte

{#if deleteConfirmationOpen}
<ConfirmationModal icon="fas fa-globe" isDangerous on:cancel={() => publicConfirmationOpen = false}
on:confirm={dispatchDelete}>
<span slot="body">Are you sure you want to delete your integration {data.name}?</span>
<span slot="confirm">Delete</span>
</ConfirmationModal>
{/if}
{#if publicConfirmationOpen}
<ConfirmationModal icon="fas fa-trash-icon" on:cancel={() => deleteConfirmationOpen = false}
on:confirm={dispatchMakePublic}>
<p slot="body">Are you sure you want to make your integration <i>{data.name}</i> public? Everyone will be able to
add it to their servers.</p>
<span slot="confirm">Confirm</span>
</ConfirmationModal>
{/if}
<div class="parent">
<div class="content">
{#if editingMetadata || editMode}
<div class="row outer-row" bind:this={metadataRow} transition:fade>
<div class="col-metadata">
<Card footer footerRight>
<span slot="title">Integration Metadata</span>
<div slot="body" class="body-wrapper">
<p>Let people know what your integration does. A preview will be generated as you type.</p>
<div class="row">
<Input col2 label="Name" placeholder="My Integration" bind:value={data.name}/>
</div>
<div class="row">
<Input col2 label="Image URL" placeholder="https://example.com/logo.png" bind:value={data.image_url}
on:change={ensureNullIfBlank}/>
<Input col2 label="Privacy Policy URL" placeholder="https://example.com/privacy"
bind:value={data.privacy_policy_url} on:change={ensureNullIfBlank}/>
</div>
<div class="row">
<Textarea col1 label="Description" placeholder="Let people know what your integration does"
bind:value={data.description}/>
</div>
</div>
<div slot="footer" style="gap: 12px">
{#if editMode}
<Button icon="fas fa-globe" disabled={data.public} on:click={confirmMakePublic}>Make Public</Button>
<Button danger icon="fas fa-trash-can" on:click={requestDeleteConfirmation}>
Delete Integration
</Button>
{:else}
<Button icon="fas fa-arrow-right" disabled={data.name.length === 0 || data.description.length === 0}
on:click={nextStage}>
Continue
</Button>
{/if}
</div>
</Card>
</div>
<div class="col-preview">
<div class="preview">
<Integration hideLinks name={data.name} imageUrl={data.image_url}>
<span slot="description">{data.description}</span>
</Integration>
</div>
</div>
</div>
{/if}
{#if !editingMetadata || editMode}
<div class="row outer-row" transition:fade>
<div class="col-left">
<Card footer footerRight>
<span slot="title">HTTP Request</span>
<div slot="body" class="body-wrapper">
<div>
<h3>API Endpoint</h3>
<div class="section">
<p>When a user opens a ticket, a HTTP <code>{data.http_method}</code> request will be sent to the
provided
request
URL. The URL must respond with a valid JSON payload.</p>
<div class="row">
<Dropdown col4 bind:value={data.http_method} label="Request Method">
<option value="GET">GET</option>
<option value="POST">POST</option>
</Dropdown>
<div class="col-3-4">
<Input col1 label="Request URL" bind:value={data.webhook_url}
placeholder="https://api.example.com/users/find?discord=%user_id%"/>
</div>
</div>
</div>
</div>
<div>
<h3>Secrets</h3>
<div class="section">
<p>
If creating a public integration, you may wish to let users provide secret values, e.g. API keys,
instead of sending all requests through your own.
</p>
<p>Note: Do not include the <code>%</code> symbols in secret names, they will be automatically
included
</p>
<div class="col">
{#each data.secrets as secret, i}
<div class="col">
<div class="row">
{#if i === 0}
<Input col1 label="Secret Name" placeholder="api_key" bind:value={secret.name}/>
{:else}
<Input col1 placeholder="api_key" bind:value={secret.name}/>
{/if}
<div class="button-anchor">
<Button danger iconOnly icon="fas fa-trash-can" on:click={() => deleteSecret(i)}/>
</div>
</div>
<div class="row">
<Textarea col1 minHeight="60px" label="Description" bind:value={secret.description}
placeholder="Tell users what value to enter for this secret, in up to 255 characters"/>
</div>
{#if i !== data.secrets.length - 1}
<hr/>
{/if}
</div>
{/each}
</div>
<Button fullWidth icon="fas fa-plus" on:click={addSecret} disabled={data.secrets.length >= 5}>
Add Additional Secret
</Button>
</div>
</div>
<div>
<h3>Request Headers</h3>
<div class="section">
<p>You can specify up to 5 HTTP headers that will be sent with the request, for example, containing
authentication
keys. You may specify the user's ID in a header, via <code>%user_id%</code>.
</p>
<p>
You may also include the values of secrets you have created, via <code>%secret_name%</code>.
{#if data.secrets.length > 0}
For example, <code>%{data.secrets[0].name}%</code>.
{/if}
</p>
<div class="col">
{#each data.headers as header, i}
<div class="row">
{#if i === 0}
<Input col2 label="Header Name" placeholder="x-auth-key" bind:value={header.name}/>
<Input col2 label="Header Value" placeholder="super secret key" bind:value={header.value}/>
<div class="button-anchor">
<Button danger iconOnly icon="fas fa-trash-can" on:click={() => deleteHeader(i)}/>
</div>
{:else}
<Input col2 placeholder="x-auth-key" bind:value={header.name}/>
<Input col2 placeholder="super secret key" bind:value={header.value}/>
<div class="button-anchor">
<Button danger iconOnly icon="fas fa-trash-can" on:click={() => deleteHeader(i)}/>
</div>
{/if}
</div>
{/each}
</div>
<Button fullWidth icon="fas fa-plus" on:click={addHeader} disabled={data.headers.length >= 5}>
Add Additional Header
</Button>
</div>
</div>
</div>
<div slot="footer">
{#if editMode}
<Button icon="fas fa-floppy-disk" on:click={dispatchSubmit}>Save</Button>
{:else }
<Button icon="fas fa-floppy-disk" on:click={dispatchSubmit}>Create</Button>
{/if}
</div>
</Card>
</div>
<div class="col-right">
<Card footer={false} fill={false}>
<span slot="title">Placeholders</span>
<div slot="body" class="body-wrapper">
<div class="section">
<p>
The response must contain a valid JSON payload. This payload will be parsed, and values can be
extracted
to use as placeholders in your welcome message.
</p>
<p>
Do <b>not</b> include the % symbols in the placeholder names. They will be included automatically.
</p>
<p>
The JSON path is the key path to access a field in the response JSON. You can use a period
(e.g. <code>user.username</code>) to access nested objects.
You will be presented with an example JSON payload as you type.
</p>
<div class="col">
{#each data.placeholders as placeholder, i}
<div class="row">
{#if i === 0}
<Input col2 label="Placeholder" placeholder="ingame_username"
bind:value={placeholder.name}/>
<Input col2 label="JSON Path" placeholder="user.username" bind:value={placeholder.json_path}/>
<div class="button-anchor">
<Button danger iconOnly icon="fas fa-trash-can" on:click={() => deletePlaceholder(i)}/>
</div>
{:else}
<Input col2 placeholder="ingame_username" bind:value={placeholder.name}/>
<Input col2 placeholder="user.username" bind:value={placeholder.json_path}/>
<div class="button-anchor">
<Button danger iconOnly icon="fas fa-trash-can" on:click={() => deletePlaceholder(i)}/>
</div>
{/if}
</div>
{/each}
</div>
<Button fullWidth icon="fas fa-plus" on:click={addPlaceholder}
disabled={data.placeholders.length >= 15}>
Add Additional Placeholder
</Button>
</div>
<div>
<h3>Example Response</h3>
<div class="section">
<p>The request must be responded to with a JSON payload in the following form:</p>
<code class="codeblock">
{exampleJson}
</code>
</div>
</div>
</div>
</Card>
</div>
</div>
{/if}
</div>
</div>
<script>
import {fade} from 'svelte/transition';
import Card from "./Card.svelte";
import Button from "./Button.svelte";
import Dropdown from "./form/Dropdown.svelte";
import Input from "./form/Input.svelte";
import Integration from "./manage/Integration.svelte";
import Textarea from "./form/Textarea.svelte";
import {createEventDispatcher, onMount} from "svelte";
import ConfirmationModal from "./ConfirmationModal.svelte";
const dispatch = createEventDispatcher();
export let guildId;
let metadataRow;
let editingMetadata = true;
let exampleJson = "{}";
export let data = {
name: "",
description: "",
image_url: "",
privacy_policy_url: "",
http_method: "GET",
placeholders: [],
headers: [],
secrets: [],
};
export let editMode = false;
let deleteConfirmationOpen = false;
let publicConfirmationOpen = false;
function requestDeleteConfirmation() {
deleteConfirmationOpen = true;
}
function confirmMakePublic() {
publicConfirmationOpen = true;
}
// on:input uses the old value!
$: data.placeholders, normalisePlaceholders();
$: data.placeholders, updateExampleJson();
$: data.secrets, normaliseSecrets();
$: data.headers, normaliseHeaders();
$: data.name, data.name = data.name.substring(0, 32);
$: data.description, data.description = data.description.substring(0, 255);
function addPlaceholder() {
data.placeholders.push({name: "", json_path: ""});
data = data;
}
function deletePlaceholder(i) {
data.placeholders.splice(i, 1);
data = data;
}
function normalisePlaceholders() {
data.placeholders = data.placeholders.map((placeholder) => {
placeholder.name = placeholder.name.replaceAll(' ', '_').replaceAll('%', '');
return placeholder;
});
}
function addHeader() {
data.headers.push({name: "", value: ""});
data = data;
}
function deleteHeader(i) {
data.headers.splice(i, 1);
data = data;
}
function normaliseHeaders() {
data.headers = data.headers.map((header) => {
header.name = header.name.replaceAll(' ', '-');
return header;
});
}
function addSecret() {
data.secrets.push({name: ""});
data = data;
}
function deleteSecret(i) {
data.secrets.splice(i, 1);
data = data;
}
function normaliseSecrets() {
data.secrets = data.secrets.map((secret) => {
secret.name = secret.name.replaceAll(' ', '_').replaceAll('%', '');
return secret;
});
}
function nextStage() {
editingMetadata = false;
metadataRow.style.display = 'none';
}
function ensureNullIfBlank() {
if (data.image_url !== undefined && data.image_url !== null && data.image_url.length === 0) {
data.image_url = null;
}
if (data.privacy_policy_url !== undefined && data.privacy_policy_url !== null && data.privacy_policy_url.length === 0) {
data.privacy_policy_url = null;
}
}
function updateExampleJson() {
try {
let obj = {};
for (const placeholder of data.placeholders) {
let split = placeholder.json_path.split(".");
let current = obj;
for (const [index, part] of split.entries()) {
if (index === split.length - 1) {
current[part] = "...";
} else {
if (current[part]) {
current = current[part];
} else {
current[part] = {};
current = current[part];
}
}
}
}
exampleJson = JSON.stringify(obj, null, 2);
} catch (e) {
exampleJson = "Invalid JSON";
}
}
function dispatchSubmit() {
ensureNullIfBlank();
dispatch("submit", data);
}
function dispatchMakePublic() {
publicConfirmationOpen = false;
dispatch("makePublic", data);
}
function dispatchDelete() {
dispatch("delete", data);
}
onMount(() => {
updateExampleJson();
if (!editMode) {
addPlaceholder();
}
});
</script>
<style>
.parent {
display: flex;
justify-content: center;
width: 100%;
height: 100%;
}
.content {
display: flex;
flex-direction: column;
align-items: flex-start;
row-gap: 5vh;
width: 96%;
height: 100%;
margin-top: 30px;
padding-bottom: 5vh;
}
.body-wrapper {
display: flex;
flex-direction: column;
row-gap: 1vh;
}
.row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
width: 100%;
}
.col-left, .col-right {
width: 50%;
}
.col-metadata {
display: flex;
flex: 1;
}
.col-preview {
display: flex;
width: 33%;
max-width: 33%;
}
.preview {
width: 100%;
}
.button-anchor {
align-self: flex-end;
margin-bottom: 8px;
}
.codeblock {
border-color: #2e3136;
background-color: #2e3136;
color: white;
outline: none;
border-radius: 4px;
padding: 8px 12px;
box-sizing: border-box;
width: 100%;
height: 100%;
display: flex;
white-space: pre-wrap;
}
.section {
display: flex;
flex-direction: column;
row-gap: 1vh;
}
hr {
border-top: 1px solid #777;
border-bottom: 0;
border-left: 0;
border-right: 0;
width: 100%;
}
@media only screen and (max-width: 950px) {
.outer-row {
flex-direction: column;
align-items: center;
}
.col-left, .col-right {
width: 100%;
}
}
</style>