feat: add S3-compatible storage provider (MinIO, Ceph, R2, etc.)
Adds a new 'S3-Compatible Storage' provider that works with any
S3-API-compatible object storage service, including MinIO, Ceph,
Cloudflare R2, Backblaze B2, and others.
Changes:
- New provider class: classes/providers/storage/s3-compatible-provider.php
- Provider key: s3compatible
- Reads user-configured endpoint URL from settings
- Uses path-style URL access (required by most S3-compatible services)
- Supports credentials via AS3CF_S3COMPAT_ACCESS_KEY_ID /
AS3CF_S3COMPAT_SECRET_ACCESS_KEY wp-config.php constants
- Disables AWS-specific features (Block Public Access, Object Ownership)
- New provider SVG icons (s3compatible.svg, -link.svg, -round.svg)
- Registered provider in main plugin class with endpoint setting support
- Updated StorageProviderSubPage to show endpoint URL input for S3-compatible
- Built pro settings bundle with rollup (Svelte 4.2.19)
- Added package.json and updated rollup.config.mjs for pro-only builds
This commit is contained in:
7
ui/components/AccessKeysDefine.svelte
Normal file
7
ui/components/AccessKeysDefine.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
export let provider;
|
||||
</script>
|
||||
|
||||
<p>{@html provider.define_access_keys_desc}</p>
|
||||
|
||||
<pre>{provider.define_access_keys_example}</pre>
|
||||
41
ui/components/AccessKeysEntry.svelte
Normal file
41
ui/components/AccessKeysEntry.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script>
|
||||
import {strings} from "../js/stores";
|
||||
|
||||
export let provider;
|
||||
export let accessKeyId = "";
|
||||
export let secretAccessKey = "";
|
||||
export let disabled = false;
|
||||
|
||||
let accessKeyIdName = "access-key-id";
|
||||
let accessKeyIdLabel = $strings.access_key_id;
|
||||
|
||||
let secretAccessKeyName = "secret-access-key";
|
||||
let secretAccessKeyLabel = $strings.secret_access_key;
|
||||
</script>
|
||||
|
||||
<p>{@html provider.enter_access_keys_desc}</p>
|
||||
|
||||
<label class="input-label" for={accessKeyIdName}>{accessKeyIdLabel}</label>
|
||||
<input
|
||||
type="text"
|
||||
id={accessKeyIdName}
|
||||
name={accessKeyIdName}
|
||||
bind:value={accessKeyId}
|
||||
minlength="20"
|
||||
size="20"
|
||||
{disabled}
|
||||
class:disabled
|
||||
>
|
||||
|
||||
<label class="input-label" for={secretAccessKeyName}>{secretAccessKeyLabel}</label>
|
||||
<input
|
||||
type="text"
|
||||
id={secretAccessKeyName}
|
||||
name={secretAccessKeyName}
|
||||
bind:value={secretAccessKey}
|
||||
autocomplete="off"
|
||||
minlength="40"
|
||||
size="40"
|
||||
{disabled}
|
||||
class:disabled
|
||||
>
|
||||
12
ui/components/AssetsPage.svelte
Normal file
12
ui/components/AssetsPage.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import {strings} from "../js/stores";
|
||||
import Page from "./Page.svelte";
|
||||
import AssetsUpgrade from "./AssetsUpgrade.svelte";
|
||||
|
||||
export let name = "assets";
|
||||
</script>
|
||||
|
||||
<Page {name} on:routeEvent>
|
||||
<h2 class="page-title">{$strings.assets_title}</h2>
|
||||
<AssetsUpgrade/>
|
||||
</Page>
|
||||
33
ui/components/AssetsUpgrade.svelte
Normal file
33
ui/components/AssetsUpgrade.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script>
|
||||
import {strings, urls} from "../js/stores";
|
||||
import Upsell from "./Upsell.svelte";
|
||||
|
||||
let benefits = [
|
||||
{
|
||||
icon: $urls.assets + 'img/icon/fonts.svg',
|
||||
alt: 'js icon',
|
||||
text: $strings.assets_uppsell_benefits.js,
|
||||
},
|
||||
{
|
||||
icon: $urls.assets + 'img/icon/css.svg',
|
||||
alt: 'css icon',
|
||||
text: $strings.assets_uppsell_benefits.css,
|
||||
},
|
||||
{
|
||||
icon: $urls.assets + 'img/icon/fonts.svg',
|
||||
alt: 'fonts icon',
|
||||
text: $strings.assets_uppsell_benefits.fonts,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Upsell benefits={benefits}>
|
||||
<div slot="heading">{$strings.assets_upsell_heading}</div>
|
||||
|
||||
<div slot="description">{@html $strings.assets_upsell_description}</div>
|
||||
|
||||
<a slot="call-to-action" href={$urls.upsell_discount_assets} class="button btn-lg btn-primary">
|
||||
<img src={$urls.assets + "img/icon/stars.svg"} alt="stars icon" style="margin-right: 5px;">
|
||||
{$strings.assets_upsell_cta}
|
||||
</a>
|
||||
</Upsell>
|
||||
57
ui/components/BackNextButtonsRow.svelte
Normal file
57
ui/components/BackNextButtonsRow.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script>
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import {strings} from "../js/stores";
|
||||
import Button from "./Button.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Back button is optional but shown by default.
|
||||
export let backText = $strings.back;
|
||||
export let backDisabled = false;
|
||||
export let backTitle = "";
|
||||
export let backVisible = false;
|
||||
|
||||
// Skip button is optional, only shown if explicitly made visible.
|
||||
export let skipText = $strings.skip;
|
||||
export let skipDisabled = false;
|
||||
export let skipTitle = "";
|
||||
export let skipVisible = false;
|
||||
|
||||
// Next button required.
|
||||
export let nextText = $strings.next;
|
||||
export let nextDisabled = false;
|
||||
export let nextTitle = "";
|
||||
</script>
|
||||
|
||||
<div class="btn-row">
|
||||
{#if backVisible}
|
||||
<Button
|
||||
large
|
||||
on:click="{() => dispatch('back')}"
|
||||
disabled={backDisabled}
|
||||
title={backTitle}
|
||||
>
|
||||
{backText}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if skipVisible}
|
||||
<Button
|
||||
large
|
||||
outline
|
||||
on:click="{() => dispatch('skip')}"
|
||||
disabled={skipDisabled}
|
||||
title={skipTitle}
|
||||
>
|
||||
{skipText}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
large
|
||||
primary
|
||||
on:click="{() => dispatch('next')}"
|
||||
disabled={nextDisabled}
|
||||
title={nextTitle}
|
||||
>
|
||||
{nextText}
|
||||
</Button>
|
||||
</div>
|
||||
427
ui/components/BucketSettingsSubPage.svelte
Normal file
427
ui/components/BucketSettingsSubPage.svelte
Normal file
@@ -0,0 +1,427 @@
|
||||
<script>
|
||||
import {
|
||||
createEventDispatcher,
|
||||
getContext,
|
||||
hasContext,
|
||||
onMount
|
||||
} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
import {slide} from "svelte/transition";
|
||||
import {
|
||||
api,
|
||||
settings,
|
||||
defined_settings,
|
||||
strings,
|
||||
storage_provider,
|
||||
urls,
|
||||
current_settings,
|
||||
needs_refresh,
|
||||
revalidatingSettings,
|
||||
state
|
||||
} from "../js/stores";
|
||||
import {scrollIntoView} from "../js/scrollIntoView";
|
||||
import {
|
||||
scrollNotificationsIntoView
|
||||
} from "../js/scrollNotificationsIntoView";
|
||||
import {needsRefresh} from "../js/needsRefresh";
|
||||
import SubPage from "./SubPage.svelte";
|
||||
import Panel from "./Panel.svelte";
|
||||
import PanelRow from "./PanelRow.svelte";
|
||||
import TabButton from "./TabButton.svelte";
|
||||
import BackNextButtonsRow from "./BackNextButtonsRow.svelte";
|
||||
import RadioButton from "./RadioButton.svelte";
|
||||
import Loading from "./Loading.svelte";
|
||||
import DefinedInWPConfig from "./DefinedInWPConfig.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Parent page may want to be locked.
|
||||
let settingsLocked = writable( false );
|
||||
|
||||
if ( hasContext( "settingsLocked" ) ) {
|
||||
settingsLocked = getContext( "settingsLocked" );
|
||||
}
|
||||
|
||||
// Keep track of where we were at prior to any changes made here.
|
||||
let initialSettings = $current_settings;
|
||||
|
||||
if ( hasContext( "initialSettings" ) ) {
|
||||
initialSettings = getContext( "initialSettings" );
|
||||
}
|
||||
|
||||
// As this page does not directly alter the settings store until done,
|
||||
// we need to keep track of any changes made elsewhere and prompt
|
||||
// the user to refresh the page.
|
||||
let saving = false;
|
||||
const previousSettings = { ...$current_settings };
|
||||
const previousDefines = { ...$defined_settings };
|
||||
|
||||
$: {
|
||||
$needs_refresh = $needs_refresh || needsRefresh( saving, previousSettings, $current_settings, previousDefines, $defined_settings );
|
||||
}
|
||||
|
||||
let bucketSource = "existing";
|
||||
let enterOrSelectExisting = "enter";
|
||||
|
||||
// If $defined_settings.bucket set, must use it, and disable change.
|
||||
let newBucket = $settings.bucket;
|
||||
$: defined = $defined_settings.includes( "bucket" );
|
||||
$: disabled = defined || $needs_refresh || $settingsLocked;
|
||||
|
||||
// If $defined_settings.region set, must use it, and disable change.
|
||||
let newRegion = $settings.region;
|
||||
$: newRegionDefined = $defined_settings.includes( "region" );
|
||||
$: newRegionDisabled = newRegionDefined || $needs_refresh || $settingsLocked;
|
||||
|
||||
/**
|
||||
* Handles clicking the Existing radio button.
|
||||
*/
|
||||
function handleExisting() {
|
||||
if ( disabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
bucketSource = "existing";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking the New radio button.
|
||||
*/
|
||||
function handleNew() {
|
||||
if ( disabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
bucketSource = "new";
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the API to get a list of existing buckets for the currently selected storage provider and region (if applicable).
|
||||
*
|
||||
* @param {string} region
|
||||
*
|
||||
* @return {Promise<*[]>}
|
||||
*/
|
||||
async function getBuckets( region ) {
|
||||
let params = {};
|
||||
|
||||
if ( $storage_provider.region_required ) {
|
||||
params = { region: region };
|
||||
}
|
||||
|
||||
let data = await api.get( "buckets", params );
|
||||
|
||||
if ( data.hasOwnProperty( "buckets" ) ) {
|
||||
if ( data.buckets.filter( ( bucket ) => bucket.Name === newBucket ).length === 0 ) {
|
||||
newBucket = "";
|
||||
}
|
||||
|
||||
return data.buckets;
|
||||
}
|
||||
|
||||
newBucket = "";
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the API to create a new bucket with the currently entered name and selected region.
|
||||
*
|
||||
* @return {Promise<boolean>}
|
||||
*/
|
||||
async function createBucket() {
|
||||
let data = await api.post( "buckets", {
|
||||
bucket: newBucket,
|
||||
region: newRegion
|
||||
} )
|
||||
|
||||
if ( data.hasOwnProperty( "saved" ) ) {
|
||||
return data.saved;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Potentially returns a reason that the provided bucket name is invalid.
|
||||
*
|
||||
* @param {string} bucket
|
||||
* @param {string} source Either "existing" or "new".
|
||||
* @param {string} existingType Either "enter" or "select".
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getInvalidBucketNameMessage( bucket, source, existingType ) {
|
||||
// If there's an invalid region defined, don't even bother looking at bucket name.
|
||||
if ( newRegionDefined && (newRegion.length === 0 || !$storage_provider.regions.hasOwnProperty( newRegion )) ) {
|
||||
return $strings.defined_region_invalid;
|
||||
}
|
||||
|
||||
const bucketNamePattern = source === "new" ? /[^a-z0-9.\-]/ : /[^a-zA-Z0-9.\-_]/;
|
||||
|
||||
let message = "";
|
||||
|
||||
if ( bucket.trim().length < 1 ) {
|
||||
if ( source === "existing" && existingType === "select" ) {
|
||||
message = $strings.no_bucket_selected;
|
||||
} else {
|
||||
message = $strings.create_bucket_name_missing;
|
||||
|
||||
}
|
||||
} else if ( true === bucketNamePattern.test( bucket ) ) {
|
||||
message = source === "new" ? $strings.create_bucket_invalid_chars : $strings.select_bucket_invalid_chars;
|
||||
} else if ( bucket.length < 3 ) {
|
||||
message = $strings.create_bucket_name_short;
|
||||
} else if ( bucket.length > 63 ) {
|
||||
message = $strings.create_bucket_name_long;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
$: invalidBucketNameMessage = getInvalidBucketNameMessage( newBucket, bucketSource, enterOrSelectExisting );
|
||||
|
||||
/**
|
||||
* Returns text to be used on Next button.
|
||||
*
|
||||
* @param {string} source Either "existing" or "new".
|
||||
* @param {string} existingType Either "enter" or "select".
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getNextText( source, existingType ) {
|
||||
if ( source === "existing" && existingType === "enter" ) {
|
||||
return $strings.save_enter_bucket;
|
||||
}
|
||||
|
||||
if ( source === "existing" && existingType === "select" ) {
|
||||
return $strings.save_select_bucket;
|
||||
}
|
||||
|
||||
if ( source === "new" ) {
|
||||
return $strings.save_new_bucket;
|
||||
}
|
||||
|
||||
return $strings.next;
|
||||
}
|
||||
|
||||
$: nextText = getNextText( bucketSource, enterOrSelectExisting );
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
if ( bucketSource === "new" && false === await createBucket() ) {
|
||||
scrollNotificationsIntoView();
|
||||
return;
|
||||
}
|
||||
|
||||
saving = true;
|
||||
state.pausePeriodicFetch();
|
||||
|
||||
$settings.bucket = newBucket;
|
||||
$settings.region = newRegion;
|
||||
const result = await settings.save();
|
||||
|
||||
// If something went wrong, don't move onto next step.
|
||||
if ( result.hasOwnProperty( "saved" ) && !result.saved ) {
|
||||
settings.reset();
|
||||
saving = false;
|
||||
await state.resumePeriodicFetch();
|
||||
|
||||
scrollNotificationsIntoView();
|
||||
return;
|
||||
}
|
||||
|
||||
$revalidatingSettings = true;
|
||||
const statePromise = state.resumePeriodicFetch();
|
||||
|
||||
result.bucketSource = bucketSource;
|
||||
result.initialSettings = initialSettings;
|
||||
|
||||
dispatch( "routeEvent", {
|
||||
event: "settings.save",
|
||||
data: result,
|
||||
default: "/"
|
||||
} );
|
||||
|
||||
// Just make sure periodic state fetch promise is done with,
|
||||
// even though we don't really care about it.
|
||||
await statePromise;
|
||||
$revalidatingSettings = false;
|
||||
}
|
||||
|
||||
onMount( () => {
|
||||
// Default to first region in storage provider if not defined and not set or not valid.
|
||||
if ( !newRegionDefined && (newRegion.length === 0 || !$storage_provider.regions.hasOwnProperty( newRegion )) ) {
|
||||
newRegion = Object.keys( $storage_provider.regions )[ 0 ];
|
||||
}
|
||||
} );
|
||||
</script>
|
||||
|
||||
<SubPage name="bucket-settings" route="/storage/bucket">
|
||||
<Panel heading={$strings.bucket_source_title} multi {defined}>
|
||||
<PanelRow class="body flex-row tab-buttons">
|
||||
<TabButton
|
||||
active={bucketSource === "existing"}
|
||||
{disabled}
|
||||
text={$strings.use_existing_bucket}
|
||||
on:click={handleExisting}
|
||||
/>
|
||||
<TabButton
|
||||
active={bucketSource === "new"}
|
||||
{disabled}
|
||||
text={$strings.create_new_bucket}
|
||||
on:click={handleNew}
|
||||
/>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
{#if bucketSource === "existing"}
|
||||
<Panel heading={$strings.existing_bucket_title} storageProvider={$storage_provider} multi {defined}>
|
||||
<PanelRow class="body flex-column">
|
||||
<div class="flex-row align-center row radio-btns">
|
||||
<RadioButton bind:selected={enterOrSelectExisting} value="enter" list {disabled}>{$strings.enter_bucket}</RadioButton>
|
||||
<RadioButton bind:selected={enterOrSelectExisting} value="select" list {disabled}>{$strings.select_bucket}</RadioButton>
|
||||
</div>
|
||||
|
||||
{#if enterOrSelectExisting === "enter"}
|
||||
<div class="flex-row align-center row">
|
||||
<div class="new-bucket-details flex-column">
|
||||
<label class="input-label" for="bucket-name">{$strings.bucket_name}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="bucket-name"
|
||||
class="bucket-name"
|
||||
name="bucket"
|
||||
minlength="3"
|
||||
placeholder={$strings.enter_bucket_name_placeholder}
|
||||
bind:value={newBucket}
|
||||
class:disabled
|
||||
{disabled}
|
||||
>
|
||||
</div>
|
||||
{#if $storage_provider.region_required}
|
||||
<div class="region flex-column">
|
||||
<label class="input-label" for="region">
|
||||
{$strings.region} <DefinedInWPConfig defined={newRegionDefined}/>
|
||||
</label>
|
||||
<select name="region" id="region" bind:value={newRegion} disabled={newRegionDisabled} class:disabled={newRegionDisabled}>
|
||||
{#each Object.entries( $storage_provider.regions ) as [regionKey, regionName], index}
|
||||
<option
|
||||
value={regionKey}
|
||||
selected={regionKey === newRegion}
|
||||
>
|
||||
{regionName}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if enterOrSelectExisting === "select"}
|
||||
{#if $storage_provider.region_required}
|
||||
<label class="input-label" for="list-region">
|
||||
{$strings.region} <DefinedInWPConfig defined={newRegionDefined}/>
|
||||
</label>
|
||||
<select name="region" id="list-region" bind:value={newRegion} disabled={newRegionDisabled} class:disabled={newRegionDisabled}>
|
||||
{#each Object.entries( $storage_provider.regions ) as [regionKey, regionName], index}
|
||||
<option
|
||||
value={regionKey}
|
||||
selected={regionKey === newRegion}
|
||||
>
|
||||
{regionName}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
{#await getBuckets( newRegion )}
|
||||
<Loading/>
|
||||
{:then buckets}
|
||||
<ul class="bucket-list">
|
||||
{#if buckets.length}
|
||||
{#each buckets as bucket}
|
||||
<!-- TODO: Fix a11y. -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<li
|
||||
class="row"
|
||||
class:active={newBucket === bucket.Name}
|
||||
on:click={() => newBucket = bucket.Name}
|
||||
use:scrollIntoView={newBucket === bucket.Name}
|
||||
data-bucket-name={bucket.Name}
|
||||
>
|
||||
<img class="icon bucket" src="{$urls.assets + 'img/icon/bucket.svg'}" alt={$strings.bucket_icon}>
|
||||
<p>{bucket.Name}</p>
|
||||
{#if newBucket === bucket.Name}
|
||||
<img class="icon status" src="{$urls.assets + 'img/icon/licence-checked.svg'}" type="image/svg+xml" alt={$strings.selected_desc}>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
{:else}
|
||||
<li class="row nothing-found">
|
||||
<p>{$strings.nothing_found}</p>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{/await}
|
||||
{/if}
|
||||
{#if invalidBucketNameMessage}
|
||||
<p class="input-error" transition:slide>{invalidBucketNameMessage}</p>
|
||||
{/if}
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
||||
{#if bucketSource === "new"}
|
||||
<Panel heading={$strings.new_bucket_title} storageProvider={$storage_provider} multi {defined}>
|
||||
<PanelRow class="body flex-column">
|
||||
<div class="flex-row align-center row">
|
||||
<div class="new-bucket-details flex-column">
|
||||
<label class="input-label" for="new-bucket-name">{$strings.bucket_name}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="new-bucket-name"
|
||||
class="bucket-name"
|
||||
name="bucket"
|
||||
minlength="3"
|
||||
placeholder={$strings.enter_bucket_name_placeholder}
|
||||
bind:value={newBucket}
|
||||
class:disabled
|
||||
{disabled}
|
||||
>
|
||||
</div>
|
||||
<div class="region flex-column">
|
||||
<label class="input-label" for="new-region">
|
||||
{$strings.region} <DefinedInWPConfig defined={newRegionDefined}/>
|
||||
</label>
|
||||
<select name="region" id="new-region" bind:value={newRegion} disabled={newRegionDisabled} class:disabled={newRegionDisabled}>
|
||||
{#each Object.entries( $storage_provider.regions ) as [regionKey, regionName], index}
|
||||
<option
|
||||
value={regionKey}
|
||||
selected={regionKey === newRegion}
|
||||
>
|
||||
{regionName}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{#if invalidBucketNameMessage}
|
||||
<p class="input-error" transition:slide>{invalidBucketNameMessage}</p>
|
||||
{/if}
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:next={handleNext}
|
||||
{nextText}
|
||||
nextDisabled={invalidBucketNameMessage || $needs_refresh || $settingsLocked}
|
||||
nextTitle={invalidBucketNameMessage}
|
||||
/>
|
||||
</SubPage>
|
||||
76
ui/components/Button.svelte
Normal file
76
ui/components/Button.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script>
|
||||
import {createEventDispatcher} from "svelte";
|
||||
|
||||
import {urls} from "../js/stores";
|
||||
|
||||
const classes = $$props.class ? $$props.class : "";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let ref = {};
|
||||
|
||||
// Button sizes, medium is the default.
|
||||
export let extraSmall = false;
|
||||
export let small = false;
|
||||
export let large = false;
|
||||
export let medium = !extraSmall && !small && !large;
|
||||
|
||||
// Button styles, outline is the default.
|
||||
export let primary = false;
|
||||
export let expandable = false;
|
||||
export let refresh = false;
|
||||
export let outline = !primary && !expandable && !refresh;
|
||||
|
||||
// Is the button disabled? Defaults to false.
|
||||
export let disabled = false;
|
||||
|
||||
// Is the button in an expanded state? Defaults to false.
|
||||
export let expanded = false;
|
||||
|
||||
// Is the button in a refreshing state? Defaults to false.
|
||||
export let refreshing = false;
|
||||
|
||||
// A button can have a title, most useful to give a reason when disabled.
|
||||
export let title = "";
|
||||
|
||||
/**
|
||||
* Catch escape key and emit a custom cancel event.
|
||||
*
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
function handleKeyup( event ) {
|
||||
if ( event.key === "Escape" ) {
|
||||
event.preventDefault();
|
||||
dispatch( "cancel" );
|
||||
}
|
||||
}
|
||||
|
||||
function refreshIcon( refreshing ) {
|
||||
return $urls.assets + 'img/icon/' + (refreshing ? 'refresh-disabled.svg' : 'refresh.svg');
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click|preventDefault
|
||||
class:btn-xs={extraSmall}
|
||||
class:btn-sm={small}
|
||||
class:btn-md={medium}
|
||||
class:btn-lg={large}
|
||||
class:btn-primary={primary}
|
||||
class:btn-outline={outline}
|
||||
class:btn-expandable={expandable}
|
||||
class:btn-disabled={disabled}
|
||||
class:btn-expanded={expanded}
|
||||
class:btn-refresh={refresh}
|
||||
class:btn-refreshing={refreshing}
|
||||
class={classes}
|
||||
{title}
|
||||
disabled={disabled || refreshing}
|
||||
bind:this={ref}
|
||||
on:focusout
|
||||
on:keyup={handleKeyup}
|
||||
>
|
||||
{#if refresh}
|
||||
<img class="icon refresh" class:refreshing src="{refreshIcon(refreshing)}" alt={title}/>
|
||||
{/if}
|
||||
<slot/>
|
||||
</button>
|
||||
66
ui/components/CheckAgain.svelte
Normal file
66
ui/components/CheckAgain.svelte
Normal file
@@ -0,0 +1,66 @@
|
||||
<script>
|
||||
import Button from "./Button.svelte";
|
||||
import {
|
||||
api,
|
||||
revalidatingSettings,
|
||||
settings_validation,
|
||||
state,
|
||||
strings
|
||||
} from "../js/stores";
|
||||
import {delayMin} from "../js/delay";
|
||||
|
||||
export let section = "";
|
||||
|
||||
$: refreshing = $revalidatingSettings;
|
||||
|
||||
let datetime = new Date( $settings_validation[ section ].timestamp * 1000 ).toString();
|
||||
|
||||
/**
|
||||
* Calls the API to revalidate settings.
|
||||
*/
|
||||
async function revalidate() {
|
||||
let start = Date.now();
|
||||
let params = {
|
||||
revalidateSettings: true,
|
||||
section: section,
|
||||
};
|
||||
|
||||
refreshing = true;
|
||||
let json = await api.get( "state", params );
|
||||
await delayMin( start, 1000 );
|
||||
state.updateState( json );
|
||||
refreshing = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="check-again">
|
||||
{#if !refreshing}
|
||||
<Button refresh {refreshing} title={$strings.check_again_desc} on:click={revalidate}>
|
||||
{$strings.check_again_title}
|
||||
</Button>
|
||||
{:else}
|
||||
<Button refresh {refreshing} title={$strings.check_again_desc}>
|
||||
{$strings.check_again_active}
|
||||
</Button>
|
||||
{/if}
|
||||
<span class="last-update" title="{datetime}">
|
||||
{$settings_validation[ section ].last_update}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div.check-again {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
white-space: nowrap;
|
||||
min-width: 6rem;
|
||||
padding-left: 0.5rem;
|
||||
color: var(--as3cf-color-gray-700);
|
||||
}
|
||||
|
||||
#as3cf-settings .check-again .last-update {
|
||||
padding-right: 2px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
12
ui/components/Checkbox.svelte
Normal file
12
ui/components/Checkbox.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
export let name = "";
|
||||
export let checked = false;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<div class="checkbox" class:locked={disabled} class:disabled>
|
||||
<label class="toggle-label" for={name}>
|
||||
<input type="checkbox" id={name} bind:checked={checked} {disabled}/>
|
||||
<slot/>
|
||||
</label>
|
||||
</div>
|
||||
9
ui/components/DefinedInWPConfig.svelte
Normal file
9
ui/components/DefinedInWPConfig.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script>
|
||||
import {strings} from "../js/stores";
|
||||
|
||||
export let defined = false;
|
||||
</script>
|
||||
|
||||
{#if defined}
|
||||
<p class="wp-config">{$strings.defined_in_wp_config}</p>
|
||||
{/if}
|
||||
199
ui/components/DeliveryPage.svelte
Normal file
199
ui/components/DeliveryPage.svelte
Normal file
@@ -0,0 +1,199 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {
|
||||
strings,
|
||||
settings,
|
||||
storage_provider,
|
||||
delivery_providers,
|
||||
delivery_provider,
|
||||
defined_settings,
|
||||
settingsLocked,
|
||||
current_settings,
|
||||
needs_refresh,
|
||||
revalidatingSettings,
|
||||
state
|
||||
} from "../js/stores";
|
||||
import {
|
||||
scrollNotificationsIntoView
|
||||
} from "../js/scrollNotificationsIntoView";
|
||||
import {needsRefresh} from "../js/needsRefresh";
|
||||
import Page from "./Page.svelte";
|
||||
import Notifications from "./Notifications.svelte";
|
||||
import Panel from "./Panel.svelte";
|
||||
import PanelRow from "./PanelRow.svelte";
|
||||
import TabButton from "./TabButton.svelte";
|
||||
import BackNextButtonsRow from "./BackNextButtonsRow.svelte";
|
||||
import HelpButton from "./HelpButton.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let name = "delivery-provider";
|
||||
export let params = {}; // Required for regex routes.
|
||||
const _params = params; // Stops compiler warning about unused params export;
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
// As this page does not directly alter the settings store until done,
|
||||
// we need to keep track of any changes made elsewhere and prompt
|
||||
// the user to refresh the page.
|
||||
let saving = false;
|
||||
const previousSettings = { ...$current_settings };
|
||||
const previousDefines = { ...$defined_settings };
|
||||
|
||||
$: {
|
||||
$needs_refresh = $needs_refresh || needsRefresh( saving, previousSettings, $current_settings, previousDefines, $defined_settings );
|
||||
}
|
||||
|
||||
// Start with a copy of the current delivery provider.
|
||||
let deliveryProvider = { ...$delivery_provider };
|
||||
|
||||
$: defined = $defined_settings.includes( "delivery-provider" );
|
||||
$: disabled = defined || $settingsLocked;
|
||||
|
||||
let serviceName = $settings[ "delivery-provider-service-name" ];
|
||||
|
||||
$: serviceNameDefined = $defined_settings.includes( "delivery-provider-service-name" );
|
||||
$: serviceNameDisabled = serviceNameDefined || $settingsLocked;
|
||||
|
||||
/**
|
||||
* Returns an array of delivery providers that can be used with the currently configured storage provider.
|
||||
*
|
||||
* @return {array}
|
||||
*/
|
||||
function supportedDeliveryProviders() {
|
||||
return Object.values( $delivery_providers ).filter(
|
||||
( provider ) => provider.supported_storage_providers.length === 0 || provider.supported_storage_providers.includes( $storage_provider.provider_key_name )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the Next button should be disabled or not and returns a suitable reason.
|
||||
*
|
||||
* @param {Object} provider
|
||||
* @param {string} providerName
|
||||
* @param {boolean} settingsLocked
|
||||
* @param {boolean} needsRefresh
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getNextDisabledMessage( provider, providerName, settingsLocked, needsRefresh ) {
|
||||
let message = "";
|
||||
|
||||
if ( settingsLocked || needsRefresh ) {
|
||||
message = $strings.settings_locked;
|
||||
} else if ( provider.provider_service_name_override_allowed && providerName.trim().length < 1 ) {
|
||||
message = $strings.no_delivery_provider_name;
|
||||
} else if ( provider.provider_service_name_override_allowed && providerName.trim().length < 4 ) {
|
||||
message = $strings.delivery_provider_name_short;
|
||||
} else if ( deliveryProvider.provider_key_name === $delivery_provider.provider_key_name && providerName === $settings[ "delivery-provider-service-name" ] ) {
|
||||
message = $strings.nothing_to_save;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
$: nextDisabledMessage = getNextDisabledMessage( deliveryProvider, serviceName, $settingsLocked, $needs_refresh );
|
||||
|
||||
/**
|
||||
* Handles choosing a different delivery provider.
|
||||
*
|
||||
* @param {Object} provider
|
||||
*/
|
||||
function handleChooseProvider( provider ) {
|
||||
if ( disabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
deliveryProvider = provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
saving = true;
|
||||
state.pausePeriodicFetch();
|
||||
|
||||
$settings[ "delivery-provider" ] = deliveryProvider.provider_key_name;
|
||||
$settings[ "delivery-provider-service-name" ] = serviceName;
|
||||
const result = await settings.save();
|
||||
|
||||
// If something went wrong, don't move onto next step.
|
||||
if ( result.hasOwnProperty( "saved" ) && !result.saved ) {
|
||||
settings.reset();
|
||||
saving = false;
|
||||
await state.resumePeriodicFetch();
|
||||
|
||||
scrollNotificationsIntoView();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$revalidatingSettings = true;
|
||||
const statePromise = state.resumePeriodicFetch();
|
||||
|
||||
dispatch( "routeEvent", {
|
||||
event: "settings.save",
|
||||
data: result,
|
||||
default: "/media/delivery"
|
||||
} );
|
||||
|
||||
// Just make sure periodic state fetch promise is done with,
|
||||
// even though we don't really care about it.
|
||||
await statePromise;
|
||||
$revalidatingSettings = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab={name} tabParent="media"/>
|
||||
<h2 class="page-title">{$strings.delivery_title}</h2>
|
||||
|
||||
<div class="delivery-provider-settings-page wrapper">
|
||||
<Panel heading={$strings.select_delivery_provider_title} defined={defined} multi>
|
||||
<PanelRow class="body flex-column delivery-provider-buttons">
|
||||
{#each supportedDeliveryProviders() as provider}
|
||||
<div class="row">
|
||||
<TabButton
|
||||
active={provider.provider_key_name === deliveryProvider.provider_key_name}
|
||||
{disabled}
|
||||
icon={provider.icon}
|
||||
text={provider.default_provider_service_name}
|
||||
on:click={() => handleChooseProvider( provider )}
|
||||
/>
|
||||
<p class="speed">{@html provider.edge_server_support_desc}</p>
|
||||
<p class="private-media">{@html provider.signed_urls_support_desc}</p>
|
||||
<HelpButton url={provider.provider_service_quick_start_url} desc={$strings.view_quick_start_guide}/>
|
||||
</div>
|
||||
{/each}
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
{#if deliveryProvider.provider_service_name_override_allowed}
|
||||
<Panel heading={$strings.enter_other_cdn_name_title} defined={serviceNameDefined} multi>
|
||||
<PanelRow class="body flex-column">
|
||||
<input
|
||||
type="text"
|
||||
class="cdn-name"
|
||||
id="cdn-name"
|
||||
name="cdn-name"
|
||||
minlength="4"
|
||||
placeholder={$strings.enter_other_cdn_name_placeholder}
|
||||
bind:value={serviceName}
|
||||
disabled={serviceNameDisabled}
|
||||
>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:next={handleNext}
|
||||
nextText={$strings.save_delivery_provider}
|
||||
nextDisabled={nextDisabledMessage}
|
||||
nextTitle={nextDisabledMessage}
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
72
ui/components/DeliverySettingsHeadingRow.svelte
Normal file
72
ui/components/DeliverySettingsHeadingRow.svelte
Normal file
@@ -0,0 +1,72 @@
|
||||
<script>
|
||||
import {hasContext, getContext} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
import {push} from "svelte-spa-router";
|
||||
import {
|
||||
delivery_provider,
|
||||
settings,
|
||||
storage_provider,
|
||||
strings,
|
||||
urls
|
||||
} from "../js/stores";
|
||||
import PanelRow from "./PanelRow.svelte";
|
||||
import Button from "./Button.svelte";
|
||||
|
||||
// Parent page may want to be locked.
|
||||
let settingsLocked = writable( false );
|
||||
|
||||
if ( hasContext( "settingsLocked" ) ) {
|
||||
settingsLocked = getContext( "settingsLocked" );
|
||||
}
|
||||
|
||||
$: providerType = $settings[ 'delivery-provider' ] === 'storage' ? 'storage' : 'delivery';
|
||||
$: providerKey = providerType === 'storage' ? $storage_provider.provider_key_name : $delivery_provider.provider_key_name;
|
||||
</script>
|
||||
|
||||
<PanelRow header gradient class="delivery {providerType} {providerKey}">
|
||||
<img src="{$delivery_provider.icon}" alt={$delivery_provider.provider_service_name}/>
|
||||
<div class="provider-details">
|
||||
<h3>{$delivery_provider.provider_service_name}</h3>
|
||||
<p class="console-details">
|
||||
<a href={$urls.delivery_provider_console_url} class="console" target="_blank" title={$strings.view_provider_console}>{$delivery_provider.console_title}</a>
|
||||
</p>
|
||||
</div>
|
||||
<Button outline on:click={() => push('/delivery/provider')} title={$strings.edit_delivery_provider} disabled={$settingsLocked}>{$strings.edit}</Button>
|
||||
</PanelRow>
|
||||
|
||||
<style>
|
||||
:global(#as3cf-settings.wpome div.panel.settings .header) img {
|
||||
width: var(--as3cf-settings-ctrl-width);
|
||||
height: var(--as3cf-settings-ctrl-width);
|
||||
}
|
||||
|
||||
.provider-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
margin-left: var(--as3cf-settings-option-indent);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:global(#as3cf-settings.wpome div.panel) .provider-details h3 {
|
||||
margin-left: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:global(#as3cf-settings.wpome div.panel) .console-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.console-details .console {
|
||||
flex: 0 1 min-content;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:global(#as3cf-settings.wpome div.panel) .console-details a[target="_blank"].console:after {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
99
ui/components/DeliverySettingsPanel.svelte
Normal file
99
ui/components/DeliverySettingsPanel.svelte
Normal file
@@ -0,0 +1,99 @@
|
||||
<script>
|
||||
import {delivery_provider, settings, strings} from "../js/stores";
|
||||
import Panel from "./Panel.svelte";
|
||||
import DeliverySettingsHeadingRow
|
||||
from "./DeliverySettingsHeadingRow.svelte";
|
||||
import SettingsValidationStatusRow from "./SettingsValidationStatusRow.svelte";
|
||||
import SettingsPanelOption from "./SettingsPanelOption.svelte";
|
||||
|
||||
/**
|
||||
* Potentially returns a reason that the provided domain name is invalid.
|
||||
*
|
||||
* @param {string} domain
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function domainValidator( domain ) {
|
||||
const domainPattern = /[^a-z0-9.-]/;
|
||||
|
||||
let message = "";
|
||||
|
||||
if ( domain.trim().length === 0 ) {
|
||||
message = $strings.domain_blank;
|
||||
} else if ( true === domainPattern.test( domain ) ) {
|
||||
message = $strings.domain_invalid_content;
|
||||
} else if ( domain.length < 3 ) {
|
||||
message = $strings.domain_too_short;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel name="settings" heading={$strings.delivery_settings_title} helpKey="delivery-provider">
|
||||
<DeliverySettingsHeadingRow/>
|
||||
<SettingsValidationStatusRow section="delivery"/>
|
||||
<SettingsPanelOption
|
||||
heading={$strings.rewrite_media_urls}
|
||||
description={$delivery_provider.rewrite_media_urls_desc}
|
||||
toggleName="serve-from-s3"
|
||||
bind:toggle={$settings["serve-from-s3"]}
|
||||
/>
|
||||
|
||||
{#if $delivery_provider.delivery_domain_allowed}
|
||||
<SettingsPanelOption
|
||||
heading={$strings.delivery_domain}
|
||||
description={$delivery_provider.delivery_domain_desc}
|
||||
toggleName="enable-delivery-domain"
|
||||
bind:toggle={$settings["enable-delivery-domain"]}
|
||||
textName="delivery-domain"
|
||||
bind:text={$settings["delivery-domain"]}
|
||||
validator={domainValidator}
|
||||
/>
|
||||
{#if $delivery_provider.use_signed_urls_key_file_allowed && $settings[ "enable-delivery-domain" ]}
|
||||
<SettingsPanelOption
|
||||
heading={$delivery_provider.signed_urls_option_name}
|
||||
description={$delivery_provider.signed_urls_option_description}
|
||||
toggleName="enable-signed-urls"
|
||||
bind:toggle={$settings["enable-signed-urls"]}
|
||||
>
|
||||
<!-- Currently only CloudFront needs a key file for signing -->
|
||||
{#if $settings[ "enable-signed-urls" ]}
|
||||
<SettingsPanelOption
|
||||
heading={$delivery_provider.signed_urls_key_id_name}
|
||||
description={$delivery_provider.signed_urls_key_id_description}
|
||||
textName="signed-urls-key-id"
|
||||
bind:text={$settings["signed-urls-key-id"]}
|
||||
nested={true}
|
||||
first={true}
|
||||
/>
|
||||
|
||||
<SettingsPanelOption
|
||||
heading={$delivery_provider.signed_urls_key_file_path_name}
|
||||
description={$delivery_provider.signed_urls_key_file_path_description}
|
||||
textName="signed-urls-key-file-path"
|
||||
bind:text={$settings["signed-urls-key-file-path"]}
|
||||
placeholder={$delivery_provider.signed_urls_key_file_path_placeholder}
|
||||
nested={true}
|
||||
/>
|
||||
|
||||
<SettingsPanelOption
|
||||
heading={$delivery_provider.signed_urls_object_prefix_name}
|
||||
description={$delivery_provider.signed_urls_object_prefix_description}
|
||||
textName="signed-urls-object-prefix"
|
||||
bind:text={$settings["signed-urls-object-prefix"]}
|
||||
placeholder="private/"
|
||||
nested={true}
|
||||
/>
|
||||
{/if}
|
||||
</SettingsPanelOption>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<SettingsPanelOption
|
||||
heading={$strings.force_https}
|
||||
description={$strings.force_https_desc}
|
||||
toggleName="force-https"
|
||||
bind:toggle={$settings["force-https"]}
|
||||
/>
|
||||
</Panel>
|
||||
8
ui/components/DeliverySettingsSubPage.svelte
Normal file
8
ui/components/DeliverySettingsSubPage.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script>
|
||||
import SubPage from "./SubPage.svelte";
|
||||
import DeliverySettingsPanel from "./DeliverySettingsPanel.svelte";
|
||||
</script>
|
||||
|
||||
<SubPage name="delivery-settings" route="/media/delivery">
|
||||
<DeliverySettingsPanel/>
|
||||
</SubPage>
|
||||
75
ui/components/Footer.svelte
Normal file
75
ui/components/Footer.svelte
Normal file
@@ -0,0 +1,75 @@
|
||||
<script>
|
||||
import {createEventDispatcher, onDestroy} from "svelte";
|
||||
import {slide} from "svelte/transition";
|
||||
import {
|
||||
revalidatingSettings,
|
||||
settings_changed,
|
||||
settings,
|
||||
strings,
|
||||
state,
|
||||
validationErrors
|
||||
} from "../js/stores";
|
||||
import {
|
||||
scrollNotificationsIntoView
|
||||
} from "../js/scrollNotificationsIntoView";
|
||||
import Button from "./Button.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let settingsStore = settings;
|
||||
export let settingsChangedStore = settings_changed;
|
||||
|
||||
let saving = false;
|
||||
|
||||
$: disabled = saving || $validationErrors.size > 0;
|
||||
|
||||
// On init, start with no validation errors.
|
||||
validationErrors.set( new Map() );
|
||||
|
||||
/**
|
||||
* Handles a Cancel button click.
|
||||
*/
|
||||
function handleCancel() {
|
||||
settingsStore.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Save button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSave() {
|
||||
saving = true;
|
||||
state.pausePeriodicFetch();
|
||||
const result = await settingsStore.save();
|
||||
$revalidatingSettings = true;
|
||||
const statePromise = state.resumePeriodicFetch();
|
||||
|
||||
// The save happened, whether anything changed or not.
|
||||
if ( result.hasOwnProperty( "saved" ) && result.hasOwnProperty( "changed_settings" ) ) {
|
||||
dispatch( "routeEvent", { event: "settings.save", data: result } );
|
||||
}
|
||||
|
||||
// After save make sure notifications are eyeballed.
|
||||
scrollNotificationsIntoView();
|
||||
saving = false;
|
||||
|
||||
// Just make sure periodic state fetch promise is done with,
|
||||
// even though we don't really care about it.
|
||||
await statePromise;
|
||||
$revalidatingSettings = false;
|
||||
}
|
||||
|
||||
// On navigation away from a component showing the footer,
|
||||
// make sure settings are reset.
|
||||
onDestroy( () => handleCancel() );
|
||||
</script>
|
||||
|
||||
{#if $settingsChangedStore}
|
||||
<div class="fixed-cta-block" transition:slide>
|
||||
<div class="buttons">
|
||||
<Button outline on:click={handleCancel}>{$strings.cancel_button}</Button>
|
||||
<Button primary on:click={handleSave} {disabled}>{$strings.save_changes}</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
15
ui/components/Header.svelte
Normal file
15
ui/components/Header.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script>
|
||||
import {config, urls} from "../js/stores";
|
||||
|
||||
$: header_img_url = $urls.assets + "img/brand/ome-medallion.svg";
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<div class="header-wrapper">
|
||||
<img class="medallion" src={header_img_url} alt="{$config.title} logo">
|
||||
<h1>{$config.title}</h1>
|
||||
<div class="licence">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
17
ui/components/HelpButton.svelte
Normal file
17
ui/components/HelpButton.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import {strings, urls, docs} from "../js/stores";
|
||||
|
||||
export let key = "";
|
||||
export let url = key && $docs.hasOwnProperty( key ) && $docs[ key ].hasOwnProperty( "url" ) ? $docs[ key ].url : "";
|
||||
export let desc = "";
|
||||
|
||||
// If desc supplied, use it, otherwise try and get via docs store or fall back to default help description.
|
||||
let alt = desc.length ? desc : (key && $docs.hasOwnProperty( key ) && $docs[ key ].hasOwnProperty( "desc" ) ? $docs[ key ].desc : $strings.help_desc);
|
||||
let title = alt;
|
||||
</script>
|
||||
|
||||
{#if url}
|
||||
<a href={url} {title} class="help" target="_blank" data-setting-key={key}>
|
||||
<img class="icon help" src="{$urls.assets + 'img/icon/help.svg'}" {alt}/>
|
||||
</a>
|
||||
{/if}
|
||||
7
ui/components/KeyFileDefine.svelte
Normal file
7
ui/components/KeyFileDefine.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
export let provider;
|
||||
</script>
|
||||
|
||||
<p>{@html provider.define_key_file_desc}</p>
|
||||
|
||||
<pre>{provider.define_key_file_example}</pre>
|
||||
15
ui/components/KeyFileEntry.svelte
Normal file
15
ui/components/KeyFileEntry.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script>
|
||||
import {strings} from "../js/stores";
|
||||
|
||||
export let provider;
|
||||
export let value = "";
|
||||
export let disabled = false;
|
||||
|
||||
let name = "key-file";
|
||||
let label = $strings.key_file;
|
||||
</script>
|
||||
|
||||
<p>{@html provider.enter_key_file_desc}</p>
|
||||
|
||||
<label class="input-label" for={name}>{label}</label>
|
||||
<textarea id={name} name={name} bind:value {disabled} class:disabled rows="10"></textarea>
|
||||
5
ui/components/Loading.svelte
Normal file
5
ui/components/Loading.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import {strings} from "../js/stores";
|
||||
</script>
|
||||
|
||||
<p>{$strings.loading}</p>
|
||||
71
ui/components/MediaPage.svelte
Normal file
71
ui/components/MediaPage.svelte
Normal file
@@ -0,0 +1,71 @@
|
||||
<script>
|
||||
import {getContext, hasContext, onMount, setContext} from "svelte";
|
||||
import {
|
||||
is_plugin_setup,
|
||||
settingsLocked,
|
||||
strings,
|
||||
settings_validation
|
||||
} from "../js/stores";
|
||||
import Page from "./Page.svelte";
|
||||
import Notifications from "./Notifications.svelte";
|
||||
import SubNav from "./SubNav.svelte";
|
||||
import SubPages from "./SubPages.svelte";
|
||||
import MediaSettings from "./MediaSettings.svelte";
|
||||
import UrlPreview from "./UrlPreview.svelte";
|
||||
import Footer from "./Footer.svelte";
|
||||
|
||||
export let name = "media";
|
||||
export let params = {}; // Required for regex routes.
|
||||
const _params = params; // Stops compiler warning for params;
|
||||
|
||||
let sidebar = null;
|
||||
let render = false;
|
||||
|
||||
if ( hasContext( 'sidebar' ) ) {
|
||||
sidebar = getContext( 'sidebar' );
|
||||
}
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
// We have a weird subnav here as both routes could be shown at same time.
|
||||
// So they are grouped, and CSS decides which is shown when width stops both from being shown.
|
||||
// The active route will determine the SubPage that is given the active class.
|
||||
const routes = {
|
||||
'*': MediaSettings,
|
||||
}
|
||||
|
||||
$: items = [
|
||||
{
|
||||
route: "/",
|
||||
title: () => $strings.storage_settings_title,
|
||||
noticeIcon: $settings_validation[ "storage" ].type
|
||||
},
|
||||
{
|
||||
route: "/media/delivery",
|
||||
title: () => $strings.delivery_settings_title,
|
||||
noticeIcon: $settings_validation[ "delivery" ].type
|
||||
}
|
||||
];
|
||||
|
||||
onMount( () => {
|
||||
if ( $is_plugin_setup ) {
|
||||
render = true;
|
||||
}
|
||||
} )
|
||||
</script>
|
||||
|
||||
<Page {name} on:routeEvent>
|
||||
{#if render}
|
||||
<Notifications tab={name}/>
|
||||
<SubNav {name} {items} subpage/>
|
||||
<SubPages {name} {routes}/>
|
||||
<UrlPreview/>
|
||||
{/if}
|
||||
</Page>
|
||||
|
||||
{#if sidebar && render}
|
||||
<svelte:component this={sidebar}/>
|
||||
{/if}
|
||||
|
||||
<Footer on:routeEvent/>
|
||||
10
ui/components/MediaSettings.svelte
Normal file
10
ui/components/MediaSettings.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script>
|
||||
import StorageSettingsSubPage from "./StorageSettingsSubPage.svelte";
|
||||
import DeliverySettingsSubPage from "./DeliverySettingsSubPage.svelte";
|
||||
|
||||
export let params = {}; // Required for regex routes.
|
||||
const _params = params; // Stops compiler warning about unused params export;
|
||||
</script>
|
||||
|
||||
<StorageSettingsSubPage/>
|
||||
<DeliverySettingsSubPage/>
|
||||
20
ui/components/Nav.svelte
Normal file
20
ui/components/Nav.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import {pages} from "../js/routes";
|
||||
import NavItem from "./NavItem.svelte";
|
||||
import OffloadStatus from "./OffloadStatus.svelte";
|
||||
</script>
|
||||
|
||||
<div class="nav">
|
||||
<div class="items">
|
||||
<ul class="nav">
|
||||
{#each $pages as tab (tab.position)}
|
||||
{#if tab.nav && tab.title}
|
||||
<NavItem {tab}/>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
<slot>
|
||||
<OffloadStatus/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
23
ui/components/NavItem.svelte
Normal file
23
ui/components/NavItem.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script>
|
||||
import {link} from "svelte-spa-router";
|
||||
import active from "svelte-spa-router/active";
|
||||
|
||||
export let tab;
|
||||
|
||||
let focus = false;
|
||||
let hover = false;
|
||||
</script>
|
||||
|
||||
<li class="nav-item" use:active={tab.routeMatcher ? tab.routeMatcher : tab.route} class:focus class:hover>
|
||||
<a
|
||||
href={tab.route}
|
||||
title={tab.title()}
|
||||
use:link
|
||||
on:focusin={() => focus = true}
|
||||
on:focusout={() => focus = false}
|
||||
on:mouseenter={() => hover = true}
|
||||
on:mouseleave={() => hover = false}
|
||||
>
|
||||
{tab.title()}
|
||||
</a>
|
||||
</li>
|
||||
131
ui/components/Notification.svelte
Normal file
131
ui/components/Notification.svelte
Normal file
@@ -0,0 +1,131 @@
|
||||
<script>
|
||||
import {notifications, strings, urls} from "../js/stores";
|
||||
import Button from "./Button.svelte";
|
||||
|
||||
const classes = $$props.class ? $$props.class : "";
|
||||
|
||||
export let notification = {};
|
||||
|
||||
export let unique_id = notification.id ? notification.id : "";
|
||||
|
||||
export let inline = notification.inline ? notification.inline : false;
|
||||
export let wordpress = notification.wordpress ? notification.wordpress : false;
|
||||
|
||||
export let success = notification.type === "success";
|
||||
export let warning = notification.type === "warning";
|
||||
export let error = notification.type === "error";
|
||||
let info = false;
|
||||
|
||||
// It's possible to set type purely by component property,
|
||||
// but we need notification.type to be correct too.
|
||||
if ( success ) {
|
||||
notification.type = "success";
|
||||
} else if ( warning ) {
|
||||
notification.type = "warning";
|
||||
} else if ( error ) {
|
||||
notification.type = "error";
|
||||
} else {
|
||||
info = true;
|
||||
notification.type = "info";
|
||||
}
|
||||
|
||||
export let heading = notification.hasOwnProperty( "heading" ) && notification.heading.trim().length ? notification.heading.trim() : "";
|
||||
export let dismissible = notification.dismissible ? notification.dismissible : false;
|
||||
export let icon = notification.icon ? notification.icon : false;
|
||||
export let plainHeading = notification.plainHeading ? notification.plainHeading : false;
|
||||
export let extra = notification.extra ? notification.extra : "";
|
||||
export let links = notification.links ? notification.links : [];
|
||||
export let expandable = false;
|
||||
export let expanded = false;
|
||||
|
||||
/**
|
||||
* Returns the icon URL for the notification.
|
||||
*
|
||||
* @param {string|boolean} icon
|
||||
* @param {string} notificationType
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getIconURL( icon, notificationType ) {
|
||||
if ( icon ) {
|
||||
return $urls.assets + "img/icon/" + icon;
|
||||
}
|
||||
|
||||
return $urls.assets + "img/icon/notification-" + notificationType + ".svg";
|
||||
}
|
||||
|
||||
$: iconURL = getIconURL( icon, notification.type );
|
||||
|
||||
// We need to change various properties and alignments if text is multiline.
|
||||
let iconHeight = 0;
|
||||
let bodyHeight = 0;
|
||||
|
||||
$: multiline = (iconHeight && bodyHeight) && bodyHeight > iconHeight;
|
||||
|
||||
/**
|
||||
* Builds a links row from an array of HTML links.
|
||||
*
|
||||
* @param {array} links
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getLinksHTML( links ) {
|
||||
if ( links.length ) {
|
||||
return links.join( " " );
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
$: linksHTML = getLinksHTML( links );
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="notification {classes}"
|
||||
class:inline
|
||||
class:wordpress
|
||||
class:success
|
||||
class:warning
|
||||
class:error
|
||||
class:info
|
||||
class:multiline
|
||||
class:expandable
|
||||
class:expanded
|
||||
>
|
||||
<div class="content">
|
||||
{#if iconURL}
|
||||
<div class="icon type" bind:clientHeight={iconHeight}>
|
||||
<img class="icon type" src={iconURL} alt="{notification.type} icon"/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="body" bind:clientHeight={bodyHeight}>
|
||||
{#if heading || dismissible || expandable}
|
||||
<div class="heading">
|
||||
{#if heading}
|
||||
{#if plainHeading}
|
||||
<p>{@html heading}</p>
|
||||
{:else}
|
||||
<h3>{@html heading}</h3>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if dismissible && expandable}
|
||||
<button class="dismiss" on:click|preventDefault={notifications.dismiss(unique_id)}>{$strings.dismiss_all}</button>
|
||||
<Button expandable {expanded} on:click={() => expanded = !expanded} title={expanded ? $strings.hide_details : $strings.show_details}></Button>
|
||||
{:else if expandable}
|
||||
<Button expandable {expanded} on:click={() => expanded = !expanded} title={expanded ? $strings.hide_details : $strings.show_details}></Button>
|
||||
{:else if dismissible}
|
||||
<button class="icon close" title={$strings["dismiss_notice"]} on:click|preventDefault={() => notifications.dismiss(unique_id)}></button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<slot/>
|
||||
{#if extra}
|
||||
<p>{@html extra}</p>
|
||||
{/if}
|
||||
{#if linksHTML}
|
||||
<p class="links">{@html linksHTML}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<slot name="details"/>
|
||||
</div>
|
||||
34
ui/components/Notifications.svelte
Normal file
34
ui/components/Notifications.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import {notifications} from "../js/stores";
|
||||
import Notification from "./Notification.svelte";
|
||||
|
||||
export let component = Notification;
|
||||
export let tab = "";
|
||||
export let tabParent = "";
|
||||
|
||||
/**
|
||||
* Render the notification or not?
|
||||
*/
|
||||
function renderNotification( notification ) {
|
||||
let not_dismissed = !notification.dismissed;
|
||||
let valid_parent_tab = notification.only_show_on_tab === tab && notification.hide_on_parent !== true;
|
||||
let valid_sub_tab = notification.only_show_on_tab === tabParent;
|
||||
let show_on_all_tabs = !notification.only_show_on_tab;
|
||||
|
||||
return not_dismissed && (valid_parent_tab || valid_sub_tab || show_on_all_tabs);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $notifications.length && Object.values( $notifications ).filter( notification => renderNotification( notification ) ).length}
|
||||
<div id="notifications" class="notifications wrapper">
|
||||
{#each $notifications as notification (notification.render_key)}
|
||||
{#if renderNotification( notification )}
|
||||
<svelte:component this={component} notification={notification}>
|
||||
{#if notification.message}
|
||||
<p>{@html notification.message}</p>
|
||||
{/if}
|
||||
</svelte:component>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
120
ui/components/OffloadStatus.svelte
Normal file
120
ui/components/OffloadStatus.svelte
Normal file
@@ -0,0 +1,120 @@
|
||||
<script>
|
||||
import {counts, strings, urls} from "../js/stores";
|
||||
import {numToString} from "../js/numToString";
|
||||
import ProgressBar from "../components/ProgressBar.svelte";
|
||||
import OffloadStatusFlyout from "./OffloadStatusFlyout.svelte";
|
||||
|
||||
// Controls whether flyout is visible or not.
|
||||
export let expanded = false;
|
||||
export let flyoutButton = {};
|
||||
export let hasFocus = false;
|
||||
|
||||
/**
|
||||
* Returns the numeric percentage progress for total offloaded media.
|
||||
*
|
||||
* @param {number} total
|
||||
* @param {number} offloaded
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
function getPercentComplete( total, offloaded ) {
|
||||
if ( total < 1 || offloaded < 1 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const percent = Math.floor( Math.abs( offloaded / total * 100 ) );
|
||||
|
||||
if ( percent > 100 ) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return percent;
|
||||
}
|
||||
|
||||
$: percentComplete = getPercentComplete( $counts.total, $counts.offloaded );
|
||||
$: complete = percentComplete >= 100;
|
||||
|
||||
/**
|
||||
* Returns a formatted title string reflecting the current status.
|
||||
*
|
||||
* @param {number} percent
|
||||
* @param {number} total
|
||||
* @param {number} offloaded
|
||||
* @param {string} description
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getTitle( percent, total, offloaded, description ) {
|
||||
return percent + "% (" + numToString( offloaded ) + "/" + numToString( total ) + ") " + description;
|
||||
}
|
||||
|
||||
$: title = getTitle( percentComplete, $counts.total, $counts.offloaded, $strings.offloaded );
|
||||
|
||||
/**
|
||||
* Handles a click to toggle the flyout.
|
||||
*/
|
||||
function handleClick() {
|
||||
expanded = !expanded;
|
||||
flyoutButton.focus();
|
||||
|
||||
// We've handled the click.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep track of when a child control gets mouse focus.
|
||||
*/
|
||||
function handleMouseEnter() {
|
||||
hasFocus = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep track of when a child control loses mouse focus.
|
||||
*/
|
||||
function handleMouseLeave() {
|
||||
hasFocus = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- TODO: Fix a11y. -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="nav-status-wrapper" class:complete>
|
||||
<!-- TODO: Fix a11y. -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="nav-status"
|
||||
{title}
|
||||
on:click|preventDefault={handleClick}
|
||||
on:mouseenter={handleMouseEnter}
|
||||
on:mouseleave={handleMouseLeave}
|
||||
>
|
||||
{#if complete}
|
||||
<img
|
||||
class="icon type"
|
||||
src={$urls.assets + "img/icon/licence-checked.svg"}
|
||||
alt="{title}"
|
||||
{title}
|
||||
/>
|
||||
{/if}
|
||||
<p
|
||||
class="status-text"
|
||||
{title}
|
||||
>
|
||||
<strong>{percentComplete}%</strong>
|
||||
<span> {@html $strings.offloaded}</span>
|
||||
</p>
|
||||
<ProgressBar
|
||||
{percentComplete}
|
||||
{title}
|
||||
/>
|
||||
</div>
|
||||
<slot name="flyout">
|
||||
<OffloadStatusFlyout bind:expanded bind:hasFocus bind:buttonRef={flyoutButton}/>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.nav-status-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
191
ui/components/OffloadStatusFlyout.svelte
Normal file
191
ui/components/OffloadStatusFlyout.svelte
Normal file
@@ -0,0 +1,191 @@
|
||||
<script>
|
||||
import {
|
||||
counts,
|
||||
offloadRemainingUpsell,
|
||||
summaryCounts,
|
||||
strings,
|
||||
urls,
|
||||
api,
|
||||
state
|
||||
} from "../js/stores";
|
||||
import {numToString} from "../js/numToString";
|
||||
import {delayMin} from "../js/delay";
|
||||
import Button from "./Button.svelte";
|
||||
import Panel from "./Panel.svelte";
|
||||
import PanelRow from "./PanelRow.svelte";
|
||||
|
||||
export let expanded = false;
|
||||
export let buttonRef = {};
|
||||
export let panelRef = {};
|
||||
export let hasFocus = false;
|
||||
export let refreshing = false;
|
||||
|
||||
/**
|
||||
* Keep track of when a child control gets mouse focus.
|
||||
*/
|
||||
function handleMouseEnter() {
|
||||
hasFocus = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep track of when a child control loses mouse focus.
|
||||
*/
|
||||
function handleMouseLeave() {
|
||||
hasFocus = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the panel is clicked, select the first focusable element
|
||||
* so that clicking outside the panel triggers a lost focus event.
|
||||
*/
|
||||
function handlePanelClick() {
|
||||
hasFocus = true;
|
||||
|
||||
const firstFocusable = panelRef.querySelector( "a:not([tabindex='-1']),button:not([tabindex='-1'])" );
|
||||
|
||||
if ( firstFocusable ) {
|
||||
firstFocusable.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When either the button or panel completely lose focus, close the flyout.
|
||||
*
|
||||
* @param {FocusEvent} event
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
function handleFocusOut( event ) {
|
||||
if ( !expanded ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mouse click and OffloadStatus control/children no longer have mouse focus.
|
||||
if ( event.relatedTarget === null && !hasFocus ) {
|
||||
expanded = false;
|
||||
}
|
||||
|
||||
// Keyboard focus change and new focused control isn't within OffloadStatus/Flyout.
|
||||
if (
|
||||
event.relatedTarget !== null &&
|
||||
event.relatedTarget !== buttonRef &&
|
||||
!panelRef.contains( event.relatedTarget )
|
||||
) {
|
||||
expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cancel event from panel and button.
|
||||
*/
|
||||
function handleCancel() {
|
||||
buttonRef.focus();
|
||||
expanded = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually refresh the media counts.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleRefresh() {
|
||||
let start = Date.now();
|
||||
refreshing = true;
|
||||
|
||||
let params = {
|
||||
refreshMediaCounts: true
|
||||
};
|
||||
|
||||
let json = await api.get( "state", params );
|
||||
await delayMin( start, 1000 );
|
||||
state.updateState( json );
|
||||
|
||||
refreshing = false;
|
||||
buttonRef.focus();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button
|
||||
expandable
|
||||
{expanded}
|
||||
on:click={() => expanded = !expanded}
|
||||
title={expanded ? $strings.hide_details : $strings.show_details}
|
||||
bind:ref={buttonRef}
|
||||
on:focusout={handleFocusOut}
|
||||
on:cancel={handleCancel}
|
||||
/>
|
||||
|
||||
{#if expanded}
|
||||
<Panel
|
||||
multi
|
||||
flyout
|
||||
refresh
|
||||
{refreshing}
|
||||
heading={$strings.offload_status_title}
|
||||
refreshDesc={$strings.refresh_media_counts_desc}
|
||||
bind:ref={panelRef}
|
||||
on:focusout={handleFocusOut}
|
||||
on:mouseenter={handleMouseEnter}
|
||||
on:mouseleave={handleMouseLeave}
|
||||
on:mousedown={handleMouseEnter}
|
||||
on:click={handlePanelClick}
|
||||
on:cancel={handleCancel}
|
||||
on:refresh={handleRefresh}
|
||||
>
|
||||
<PanelRow class="summary">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{$strings.summary_type_title}</th>
|
||||
<th class="numeric">{$strings.summary_offloaded_title}</th>
|
||||
<th class="numeric">{$strings.summary_not_offloaded_title}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{#each $summaryCounts as summary (summary.type)}
|
||||
<tr>
|
||||
<td>{summary.name}</td>
|
||||
{#if summary.offloaded_url}
|
||||
<td class="numeric">
|
||||
<a href="{summary.offloaded_url}">{numToString( summary.offloaded )}</a>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="numeric">{numToString( summary.offloaded )}</td>
|
||||
{/if}
|
||||
{#if summary.not_offloaded_url}
|
||||
<td class="numeric">
|
||||
<a href="{summary.not_offloaded_url}">{numToString( summary.not_offloaded )}</a>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="numeric">{numToString( summary.not_offloaded )}</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
{#if $summaryCounts.length > 1}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>{$strings.summary_total_row_title}</td>
|
||||
<td class="numeric">{numToString( $counts.offloaded )}</td>
|
||||
<td class="numeric">{numToString( $counts.not_offloaded )}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{/if}
|
||||
</table>
|
||||
</PanelRow>
|
||||
|
||||
<slot name="footer">
|
||||
<PanelRow footer class="upsell">
|
||||
{#if $offloadRemainingUpsell}
|
||||
<p>{@html $offloadRemainingUpsell}</p>
|
||||
{/if}
|
||||
<a href={$urls.upsell_discount} class="button btn-sm btn-primary licence" target="_blank">
|
||||
<img src={$urls.assets + "img/icon/stars.svg"} alt="stars icon" style="margin-right: 5px;">
|
||||
{$strings.offload_remaining_upsell_cta}
|
||||
</a>
|
||||
</PanelRow>
|
||||
</slot>
|
||||
</Panel>
|
||||
{/if}
|
||||
33
ui/components/Page.svelte
Normal file
33
ui/components/Page.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script>
|
||||
import {onMount, createEventDispatcher, setContext} from "svelte";
|
||||
import {location} from "svelte-spa-router";
|
||||
import {current_settings} from "../js/stores";
|
||||
|
||||
export let name = "";
|
||||
|
||||
// In some scenarios a Page should have some SubPage behaviours.
|
||||
export let subpage = false;
|
||||
|
||||
export let initialSettings = $current_settings;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// When a page is created, store a copy of the initial settings
|
||||
// so they can be compared with any changes later.
|
||||
setContext( "initialSettings", initialSettings );
|
||||
|
||||
// Tell the route event handlers about the initial settings too.
|
||||
onMount( () => {
|
||||
dispatch( "routeEvent", {
|
||||
event: "page.initial.settings",
|
||||
data: {
|
||||
settings: initialSettings,
|
||||
location: $location
|
||||
}
|
||||
} );
|
||||
} );
|
||||
</script>
|
||||
|
||||
<div class="page-wrapper {name}" class:subpage>
|
||||
<slot/>
|
||||
</div>
|
||||
38
ui/components/Pages.svelte
Normal file
38
ui/components/Pages.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script>
|
||||
import Router from "svelte-spa-router";
|
||||
import {push} from "svelte-spa-router";
|
||||
import {pages, routes} from "../js/routes";
|
||||
import Nav from "./Nav.svelte";
|
||||
|
||||
// These components can be overridden.
|
||||
export let nav = Nav;
|
||||
|
||||
const classes = $$props.class ? $$props.class : "";
|
||||
|
||||
/**
|
||||
* Handles events published by the router.
|
||||
*
|
||||
* This handler gives pages a chance to put their hand up and
|
||||
* provide a new route to be navigated to in response
|
||||
* to some event.
|
||||
* e.g. settings saved resulting in a question being asked.
|
||||
*
|
||||
* @param {Object} event
|
||||
*/
|
||||
function handleRouteEvent( event ) {
|
||||
const route = pages.handleRouteEvent( event.detail );
|
||||
|
||||
if ( route ) {
|
||||
push( route );
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:component this={nav}/>
|
||||
|
||||
<div class="wpome-wrapper {classes}">
|
||||
<Router routes={$routes} on:routeEvent={handleRouteEvent}/>
|
||||
<slot>
|
||||
<!-- EXTRA CONTENT GOES HERE -->
|
||||
</slot>
|
||||
</div>
|
||||
139
ui/components/Panel.svelte
Normal file
139
ui/components/Panel.svelte
Normal file
@@ -0,0 +1,139 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext, hasContext} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
import {fade} from "svelte/transition";
|
||||
import {link} from "svelte-spa-router";
|
||||
import {defined_settings, strings} from "../js/stores";
|
||||
import PanelContainer from "./PanelContainer.svelte";
|
||||
import PanelRow from "./PanelRow.svelte";
|
||||
import DefinedInWPConfig from "./DefinedInWPConfig.svelte";
|
||||
import ToggleSwitch from "./ToggleSwitch.svelte";
|
||||
import HelpButton from "./HelpButton.svelte";
|
||||
import Button from "./Button.svelte";
|
||||
|
||||
const classes = $$props.class ? $$props.class : "";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let ref = {};
|
||||
export let name = "";
|
||||
export let heading = "";
|
||||
export let defined = false;
|
||||
export let multi = false;
|
||||
export let flyout = false;
|
||||
export let toggleName = "";
|
||||
export let toggle = false;
|
||||
export let refresh = false;
|
||||
export let refreshText = $strings.refresh_title;
|
||||
export let refreshDesc = refreshText;
|
||||
export let refreshing = false;
|
||||
export let helpKey = "";
|
||||
export let helpURL = "";
|
||||
export let helpDesc = $strings.help_desc;
|
||||
|
||||
// We can display storage provider info on the right-hand side of the panel's header.
|
||||
// In the future, if anything else needs to be displayed in the same position we
|
||||
// should create a named slot or assignable component. CSS changes would be required.
|
||||
export let storageProvider = null;
|
||||
|
||||
// Parent page may want to be locked.
|
||||
let settingsLocked = writable( false );
|
||||
|
||||
if ( hasContext( "settingsLocked" ) ) {
|
||||
settingsLocked = getContext( "settingsLocked" );
|
||||
}
|
||||
|
||||
$: locked = $settingsLocked;
|
||||
$: toggleDisabled = $defined_settings.includes( toggleName ) || locked;
|
||||
|
||||
/**
|
||||
* If appropriate, clicking the header toggles to toggle switch.
|
||||
*/
|
||||
function headingClickHandler() {
|
||||
if ( toggleName && !toggleDisabled ) {
|
||||
toggle = !toggle;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch escape key and emit a custom cancel event.
|
||||
*
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
function handleKeyup( event ) {
|
||||
if ( event.key === "Escape" ) {
|
||||
event.preventDefault();
|
||||
dispatch( "cancel" );
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- TODO: Fix a11y. -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="panel {name}"
|
||||
class:multi
|
||||
class:flyout
|
||||
class:locked
|
||||
transition:fade={{duration: flyout ? 200 : 0}}
|
||||
bind:this={ref}
|
||||
on:focusout
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:mousedown
|
||||
on:click
|
||||
on:keyup={handleKeyup}
|
||||
>
|
||||
{#if !multi && heading}
|
||||
<div class="heading">
|
||||
<h2>{heading}</h2>
|
||||
{#if helpURL}
|
||||
<HelpButton url={helpURL} desc={helpDesc}/>
|
||||
{:else if helpKey}
|
||||
<HelpButton key={helpKey} desc={helpDesc}/>
|
||||
{/if}
|
||||
<DefinedInWPConfig {defined}/>
|
||||
</div>
|
||||
{/if}
|
||||
<PanelContainer class={classes}>
|
||||
{#if multi && heading}
|
||||
<PanelRow header>
|
||||
{#if toggleName}
|
||||
<ToggleSwitch name={toggleName} bind:checked={toggle} disabled={toggleDisabled}>
|
||||
{heading}
|
||||
</ToggleSwitch>
|
||||
<!-- TODO: Fix a11y. -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<h3 on:click={headingClickHandler} class="toggler" class:toggleDisabled>{heading}</h3>
|
||||
{:else}
|
||||
<h3>{heading}</h3>
|
||||
{/if}
|
||||
<DefinedInWPConfig {defined}/>
|
||||
{#if refresh}
|
||||
<Button refresh {refreshing} title={refreshDesc} on:click={() => dispatch("refresh")}>{@html refreshText}</Button>
|
||||
{/if}
|
||||
{#if storageProvider}
|
||||
<div class="provider">
|
||||
<a href="/storage/provider" use:link class="link">
|
||||
<img src={storageProvider.link_icon} alt={storageProvider.icon_desc}>
|
||||
{storageProvider.provider_service_name}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
{#if helpURL}
|
||||
<HelpButton url={helpURL} desc={helpDesc}/>
|
||||
{:else if helpKey}
|
||||
<HelpButton key={helpKey} desc={helpDesc}/>
|
||||
{/if}
|
||||
</PanelRow>
|
||||
{/if}
|
||||
|
||||
<slot/>
|
||||
</PanelContainer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toggler:not(.toggleDisabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
7
ui/components/PanelContainer.svelte
Normal file
7
ui/components/PanelContainer.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
const classes = $$props.class ? $$props.class : "";
|
||||
</script>
|
||||
|
||||
<div class="panel-container {classes}">
|
||||
<slot/>
|
||||
</div>
|
||||
30
ui/components/PanelRow.svelte
Normal file
30
ui/components/PanelRow.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script>
|
||||
const classes = $$props.class ? $$props.class : "";
|
||||
|
||||
export let header = false;
|
||||
export let footer = false;
|
||||
export let gradient = false;
|
||||
</script>
|
||||
|
||||
<div class="panel-row {classes}" class:header class:footer>
|
||||
{#if gradient}
|
||||
<div class="gradient"></div>
|
||||
{/if}
|
||||
<slot/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.panel-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header .gradient {
|
||||
position: absolute;
|
||||
width: 144px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
transform: matrix(-1, 0, 0, 1, 0, 0);
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
52
ui/components/ProgressBar.svelte
Normal file
52
ui/components/ProgressBar.svelte
Normal file
@@ -0,0 +1,52 @@
|
||||
<script>
|
||||
import {cubicOut} from "svelte/easing";
|
||||
import {tweened} from "svelte/motion";
|
||||
|
||||
export let percentComplete = 0;
|
||||
export let starting = false;
|
||||
export let running = false;
|
||||
export let paused = false;
|
||||
export let title = "";
|
||||
|
||||
let progressTweened = tweened( 0, {
|
||||
duration: 400,
|
||||
easing: cubicOut
|
||||
} );
|
||||
|
||||
/**
|
||||
* Utility function for reactively getting the progress.
|
||||
*
|
||||
* @param {number} percent
|
||||
*
|
||||
* @return {number|*}
|
||||
*/
|
||||
function getProgress( percent ) {
|
||||
if ( percent < 1 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ( percent >= 100 ) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return percent;
|
||||
}
|
||||
|
||||
$: progressTweened.set( getProgress( percentComplete ) );
|
||||
$: complete = percentComplete >= 100;
|
||||
</script>
|
||||
|
||||
<!-- TODO: Fix a11y. -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="progress-bar"
|
||||
class:stripe={running && ! paused}
|
||||
class:animate={starting}
|
||||
{title}
|
||||
on:click|preventDefault
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
>
|
||||
<span class="indicator animate" class:complete class:running style="width: {$progressTweened}%"></span>
|
||||
</div>
|
||||
20
ui/components/RadioButton.svelte
Normal file
20
ui/components/RadioButton.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script>
|
||||
export let list = false;
|
||||
export let disabled = false;
|
||||
export let name = "options";
|
||||
export let value = "";
|
||||
export let selected = "";
|
||||
export let desc = "";
|
||||
</script>
|
||||
|
||||
<div class="radio-btn" class:list class:disabled>
|
||||
<label>
|
||||
<input type="radio" {name} bind:group={selected} {value} {disabled}>
|
||||
<slot/>
|
||||
</label>
|
||||
</div>
|
||||
{#if selected === value && desc}
|
||||
<p class="radio-desc">
|
||||
{@html desc}
|
||||
</p>
|
||||
{/if}
|
||||
282
ui/components/SecuritySubPage.svelte
Normal file
282
ui/components/SecuritySubPage.svelte
Normal file
@@ -0,0 +1,282 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext, hasContext} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
import {slide} from "svelte/transition";
|
||||
import {
|
||||
api,
|
||||
settings,
|
||||
strings,
|
||||
current_settings,
|
||||
storage_provider,
|
||||
delivery_provider,
|
||||
needs_refresh,
|
||||
revalidatingSettings,
|
||||
state,
|
||||
defined_settings
|
||||
} from "../js/stores";
|
||||
import {
|
||||
scrollNotificationsIntoView
|
||||
} from "../js/scrollNotificationsIntoView";
|
||||
import {needsRefresh} from "../js/needsRefresh";
|
||||
import SubPage from "./SubPage.svelte";
|
||||
import Panel from "./Panel.svelte";
|
||||
import PanelRow from "./PanelRow.svelte";
|
||||
import BackNextButtonsRow from "./BackNextButtonsRow.svelte";
|
||||
import Checkbox from "./Checkbox.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Parent page may want to be locked.
|
||||
let settingsLocked = writable( false );
|
||||
|
||||
if ( hasContext( "settingsLocked" ) ) {
|
||||
settingsLocked = getContext( "settingsLocked" );
|
||||
}
|
||||
|
||||
// As this page does not directly alter the settings store until done,
|
||||
// we need to keep track of any changes made elsewhere and prompt
|
||||
// the user to refresh the page.
|
||||
let saving = false;
|
||||
const previousSettings = { ...$current_settings };
|
||||
const previousDefines = { ...$defined_settings };
|
||||
|
||||
$: {
|
||||
$needs_refresh = $needs_refresh || needsRefresh( saving, previousSettings, $current_settings, previousDefines, $defined_settings );
|
||||
}
|
||||
|
||||
let blockPublicAccess = $settings[ "block-public-access" ];
|
||||
let bapaSetupConfirmed = false;
|
||||
|
||||
let objectOwnershipEnforced = $settings[ "object-ownership-enforced" ];
|
||||
let ooeSetupConfirmed = false;
|
||||
|
||||
// During initial setup we show a slightly different page
|
||||
// if ACLs disabled but unsupported by Delivery Provider.
|
||||
let initialSetup = false;
|
||||
|
||||
if ( hasContext( "initialSetup" ) ) {
|
||||
initialSetup = getContext( "initialSetup" );
|
||||
}
|
||||
|
||||
// If provider has changed, then still treat as initial setup.
|
||||
if (
|
||||
!initialSetup &&
|
||||
hasContext( "initialSettings" ) &&
|
||||
getContext( "initialSettings" ).provider !== $current_settings.provider
|
||||
) {
|
||||
initialSetup = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls API to update the properties of the current bucket.
|
||||
*
|
||||
* @return {Promise<boolean|*>}
|
||||
*/
|
||||
async function updateBucketProperties() {
|
||||
let data = await api.put( "buckets", {
|
||||
bucket: $settings.bucket,
|
||||
blockPublicAccess: blockPublicAccess,
|
||||
objectOwnershipEnforced: objectOwnershipEnforced
|
||||
} );
|
||||
|
||||
if ( data.hasOwnProperty( "saved" ) ) {
|
||||
return data.saved;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns text to be displayed on Next button.
|
||||
*
|
||||
* @param {boolean} bapaCurrent
|
||||
* @param {boolean} bapaNew
|
||||
* @param {boolean} ooeCurrent
|
||||
* @param {boolean} ooeNew
|
||||
* @param {boolean} needsRefresh
|
||||
* @param {boolean} settingsLocked
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getNextText( bapaCurrent, bapaNew, ooeCurrent, ooeNew, needsRefresh, settingsLocked ) {
|
||||
if ( needsRefresh || settingsLocked ) {
|
||||
return $strings.settings_locked;
|
||||
}
|
||||
|
||||
if ( bapaCurrent !== bapaNew || ooeCurrent !== ooeNew ) {
|
||||
return $strings.update_bucket_security;
|
||||
}
|
||||
|
||||
return $strings.keep_bucket_security;
|
||||
}
|
||||
|
||||
$: nextText = getNextText(
|
||||
$current_settings[ "block-public-access" ],
|
||||
blockPublicAccess,
|
||||
$current_settings[ "object-ownership-enforced" ],
|
||||
objectOwnershipEnforced,
|
||||
$needs_refresh,
|
||||
$settingsLocked
|
||||
);
|
||||
|
||||
/**
|
||||
* Determines whether the Next button should be disabled or not.
|
||||
*
|
||||
* If the delivery provider supports the security setting, then do not enable it until setup confirmed.
|
||||
*
|
||||
* All other scenarios result in safe results or warned against repercussions that are being explicitly ignored.
|
||||
*
|
||||
* @param {boolean} currentValue
|
||||
* @param {boolean} newValue
|
||||
* @param {boolean} supported
|
||||
* @param {boolean} setupConfirmed
|
||||
* @param {boolean} needsRefresh
|
||||
* @param {boolean} settingsLocked
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function getNextDisabled( currentValue, newValue, supported, setupConfirmed, needsRefresh, settingsLocked ) {
|
||||
return needsRefresh || settingsLocked || (!currentValue && newValue && supported && !setupConfirmed);
|
||||
}
|
||||
|
||||
$: nextDisabled =
|
||||
getNextDisabled(
|
||||
$current_settings[ "block-public-access" ],
|
||||
blockPublicAccess,
|
||||
$delivery_provider.block_public_access_supported,
|
||||
bapaSetupConfirmed,
|
||||
$needs_refresh,
|
||||
$settingsLocked
|
||||
) ||
|
||||
getNextDisabled(
|
||||
$current_settings[ "object-ownership-enforced" ],
|
||||
objectOwnershipEnforced,
|
||||
$delivery_provider.object_ownership_supported,
|
||||
ooeSetupConfirmed,
|
||||
$needs_refresh,
|
||||
$settingsLocked
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
if (
|
||||
blockPublicAccess === $current_settings[ "block-public-access" ] &&
|
||||
objectOwnershipEnforced === $current_settings[ "object-ownership-enforced" ]
|
||||
) {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
return;
|
||||
}
|
||||
|
||||
saving = true;
|
||||
state.pausePeriodicFetch();
|
||||
|
||||
const result = await updateBucketProperties();
|
||||
|
||||
// Regardless of whether update succeeded or not, make sure settings are up-to-date.
|
||||
await settings.fetch();
|
||||
|
||||
if ( false === result ) {
|
||||
saving = false;
|
||||
await state.resumePeriodicFetch();
|
||||
|
||||
scrollNotificationsIntoView();
|
||||
return;
|
||||
}
|
||||
|
||||
$revalidatingSettings = true;
|
||||
const statePromise = state.resumePeriodicFetch();
|
||||
|
||||
// Block All Public Access changed.
|
||||
dispatch( "routeEvent", {
|
||||
event: "bucket-security",
|
||||
data: {
|
||||
blockPublicAccess: $settings[ "block-public-access" ],
|
||||
objectOwnershipEnforced: $settings[ "object-ownership-enforced" ]
|
||||
},
|
||||
default: "/"
|
||||
} );
|
||||
|
||||
// Just make sure periodic state fetch promise is done with,
|
||||
// even though we don't really care about it.
|
||||
await statePromise;
|
||||
$revalidatingSettings = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<SubPage name="bapa-settings" route="/storage/security">
|
||||
<Panel
|
||||
class="toggle-header"
|
||||
heading={$strings.block_public_access_title}
|
||||
toggleName="block-public-access"
|
||||
bind:toggle={blockPublicAccess}
|
||||
helpKey="block-public-access"
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
{#if initialSetup && $current_settings[ "block-public-access" ] && !$delivery_provider.block_public_access_supported}
|
||||
<p>{@html $strings.block_public_access_enabled_setup_sub}</p>
|
||||
<p>{@html $delivery_provider.block_public_access_enabled_unsupported_setup_desc} {@html $storage_provider.block_public_access_enabled_unsupported_setup_desc}</p>
|
||||
{:else if $current_settings[ "block-public-access" ] && $delivery_provider.block_public_access_supported}
|
||||
<p>{@html $strings.block_public_access_enabled_sub}</p>
|
||||
<p>{@html $delivery_provider.block_public_access_enabled_supported_desc} {@html $storage_provider.block_public_access_enabled_supported_desc}</p>
|
||||
{:else if $current_settings[ "block-public-access" ] && !$delivery_provider.block_public_access_supported}
|
||||
<p>{@html $strings.block_public_access_enabled_sub}</p>
|
||||
<p>{@html $delivery_provider.block_public_access_enabled_unsupported_desc} {@html $storage_provider.block_public_access_enabled_unsupported_desc}</p>
|
||||
{:else if !$current_settings[ "block-public-access" ] && $delivery_provider.block_public_access_supported}
|
||||
<p>{@html $strings.block_public_access_disabled_sub}</p>
|
||||
<p>{@html $delivery_provider.block_public_access_disabled_supported_desc} {@html $storage_provider.block_public_access_disabled_supported_desc}</p>
|
||||
{:else}
|
||||
<p>{@html $strings.block_public_access_disabled_sub}</p>
|
||||
<p>{@html $delivery_provider.block_public_access_disabled_unsupported_desc} {@html $storage_provider.block_public_access_disabled_unsupported_desc}</p>
|
||||
{/if}
|
||||
</PanelRow>
|
||||
{#if !$current_settings[ "block-public-access" ] && blockPublicAccess && $delivery_provider.block_public_access_supported}
|
||||
<div transition:slide>
|
||||
<PanelRow class="body flex-column toggle-reveal" footer>
|
||||
<Checkbox name="confirm-setup-bapa-oai" bind:checked={bapaSetupConfirmed} disabled={$needs_refresh || $settingsLocked}>{@html $delivery_provider.block_public_access_confirm_setup_prompt}</Checkbox>
|
||||
</PanelRow>
|
||||
</div>
|
||||
{/if}
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
class="toggle-header"
|
||||
heading={$strings.object_ownership_title}
|
||||
toggleName="object-ownership-enforced"
|
||||
bind:toggle={objectOwnershipEnforced}
|
||||
helpKey="object-ownership-enforced"
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
{#if initialSetup && $current_settings[ "object-ownership-enforced" ] && !$delivery_provider.object_ownership_supported}
|
||||
<p>{@html $strings.object_ownership_enforced_setup_sub}</p>
|
||||
<p>{@html $delivery_provider.object_ownership_enforced_unsupported_setup_desc} {@html $storage_provider.object_ownership_enforced_unsupported_setup_desc}</p>
|
||||
{:else if $current_settings[ "object-ownership-enforced" ] && $delivery_provider.object_ownership_supported}
|
||||
<p>{@html $strings.object_ownership_enforced_sub}</p>
|
||||
<p>{@html $delivery_provider.object_ownership_enforced_supported_desc} {@html $storage_provider.object_ownership_enforced_supported_desc}</p>
|
||||
{:else if $current_settings[ "object-ownership-enforced" ] && !$delivery_provider.object_ownership_supported}
|
||||
<p>{@html $strings.object_ownership_enforced_sub}</p>
|
||||
<p>{@html $delivery_provider.object_ownership_enforced_unsupported_desc} {@html $storage_provider.object_ownership_enforced_unsupported_desc}</p>
|
||||
{:else if !$current_settings[ "object-ownership-enforced" ] && $delivery_provider.object_ownership_supported}
|
||||
<p>{@html $strings.object_ownership_not_enforced_sub}</p>
|
||||
<p>{@html $delivery_provider.object_ownership_not_enforced_supported_desc} {@html $storage_provider.object_ownership_not_enforced_supported_desc}</p>
|
||||
{:else}
|
||||
<p>{@html $strings.object_ownership_not_enforced_sub}</p>
|
||||
<p>{@html $delivery_provider.object_ownership_not_enforced_unsupported_desc} {@html $storage_provider.object_ownership_not_enforced_unsupported_desc}</p>
|
||||
{/if}
|
||||
</PanelRow>
|
||||
{#if !$current_settings[ "object-ownership-enforced" ] && objectOwnershipEnforced && $delivery_provider.object_ownership_supported}
|
||||
<div transition:slide>
|
||||
<PanelRow class="body flex-column toggle-reveal">
|
||||
<Checkbox name="confirm-setup-ooe-oai" bind:checked={ooeSetupConfirmed} disabled={$needs_refresh || $settingsLocked}>{@html $delivery_provider.object_ownership_confirm_setup_prompt}</Checkbox>
|
||||
</PanelRow>
|
||||
</div>
|
||||
{/if}
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow on:next={handleNext} {nextText} {nextDisabled}/>
|
||||
</SubPage>
|
||||
54
ui/components/SettingNotifications.svelte
Normal file
54
ui/components/SettingNotifications.svelte
Normal file
@@ -0,0 +1,54 @@
|
||||
<script>
|
||||
import {settings_notifications} from "../js/stores";
|
||||
import Notification from "./Notification.svelte";
|
||||
|
||||
export let settingKey;
|
||||
|
||||
/**
|
||||
* Compares two notification objects to sort them into a preferred order.
|
||||
*
|
||||
* Order should be errors, then warnings and finally anything else alphabetically by type.
|
||||
* As these (inline) notifications are typically displayed under a setting,
|
||||
* this ensures the most important notifications are nearest the control.
|
||||
*
|
||||
* @param {Object} a
|
||||
* @param {Object} b
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
function compareNotificationTypes( a, b ) {
|
||||
// Sort errors to the top.
|
||||
if ( a.type === "error" && b.type !== "error" ) {
|
||||
return -1;
|
||||
}
|
||||
if ( b.type === "error" && a.type !== "error" ) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Next sort warnings.
|
||||
if ( a.type === "warning" && b.type !== "warning" ) {
|
||||
return -1;
|
||||
}
|
||||
if ( b.type === "warning" && a.type !== "warning" ) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Anything else, just sort by type for stability.
|
||||
if ( a.type < b.type ) {
|
||||
return -1;
|
||||
}
|
||||
if ( b.type < a.type ) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $settings_notifications.has( settingKey )}
|
||||
{#each [...$settings_notifications.get( settingKey ).values()].sort( compareNotificationTypes ) as notification (notification)}
|
||||
<Notification {notification}>
|
||||
<p>{@html notification.message}</p>
|
||||
</Notification>
|
||||
{/each}
|
||||
{/if}
|
||||
37
ui/components/Settings.svelte
Normal file
37
ui/components/Settings.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
import {onMount} from "svelte";
|
||||
import {config, notifications, settings, state} from "../js/stores";
|
||||
import Header from "./Header.svelte";
|
||||
|
||||
// These components can be overridden.
|
||||
export let header = Header;
|
||||
export let footer = null;
|
||||
|
||||
// We need a disassociated copy of the initial settings to work with.
|
||||
settings.set( { ...$config.settings } );
|
||||
|
||||
// We might have some initial notifications to display too.
|
||||
if ( $config.notifications.length ) {
|
||||
for ( const notification of $config.notifications ) {
|
||||
notifications.add( notification );
|
||||
}
|
||||
}
|
||||
|
||||
onMount( () => {
|
||||
// Periodically check the state.
|
||||
state.startPeriodicFetch();
|
||||
|
||||
// Be a good citizen and clean up the timer when exiting our settings.
|
||||
return () => state.stopPeriodicFetch();
|
||||
} );
|
||||
</script>
|
||||
|
||||
{#if header}
|
||||
<svelte:component this={header}/>
|
||||
{/if}
|
||||
<slot>
|
||||
<!-- CONTENT GOES HERE -->
|
||||
</slot>
|
||||
{#if footer}
|
||||
<svelte:component this={footer}/>
|
||||
{/if}
|
||||
152
ui/components/SettingsPanelOption.svelte
Normal file
152
ui/components/SettingsPanelOption.svelte
Normal file
@@ -0,0 +1,152 @@
|
||||
<script>
|
||||
import {getContext, hasContext} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
import {slide} from "svelte/transition";
|
||||
import {defined_settings, validationErrors} from "../js/stores";
|
||||
import PanelRow from "./PanelRow.svelte";
|
||||
import ToggleSwitch from "./ToggleSwitch.svelte";
|
||||
import DefinedInWPConfig from "./DefinedInWPConfig.svelte";
|
||||
import SettingNotifications from "./SettingNotifications.svelte";
|
||||
|
||||
export let heading = "";
|
||||
export let description = "";
|
||||
export let placeholder = "";
|
||||
export let nested = false;
|
||||
export let first = false; // of nested items
|
||||
|
||||
// Toggle and Text may both be used at same time.
|
||||
export let toggleName = "";
|
||||
export let toggle = false;
|
||||
export let textName = "";
|
||||
export let text = "";
|
||||
export let alwaysShowText = false;
|
||||
|
||||
export let definedSettings = defined_settings;
|
||||
|
||||
/**
|
||||
* Optional validator function.
|
||||
*
|
||||
* @param {string} textValue
|
||||
*
|
||||
* @return {string} Empty if no error
|
||||
*/
|
||||
export let validator = ( textValue ) => "";
|
||||
|
||||
// Parent page may want to be locked.
|
||||
let settingsLocked = writable( false );
|
||||
|
||||
let textDirty = false;
|
||||
|
||||
if ( hasContext( "settingsLocked" ) ) {
|
||||
settingsLocked = getContext( "settingsLocked" );
|
||||
}
|
||||
|
||||
$: locked = $settingsLocked;
|
||||
$: toggleDisabled = $definedSettings.includes( toggleName ) || locked;
|
||||
$: textDisabled = $definedSettings.includes( textName ) || locked;
|
||||
|
||||
$: input = ((toggleName && toggle) || !toggleName || alwaysShowText) && textName;
|
||||
$: headingName = input ? textName + "-heading" : toggleName;
|
||||
|
||||
/**
|
||||
* Validate the text if validator function supplied.
|
||||
*
|
||||
* @param {string} text
|
||||
* @param {bool} toggle
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function validateText( text, toggle ) {
|
||||
let message = "";
|
||||
|
||||
if ( validator !== undefined && toggle && !textDisabled ) {
|
||||
message = validator( text );
|
||||
}
|
||||
|
||||
validationErrors.update( _validationErrors => {
|
||||
if ( _validationErrors.has( textName ) && message === "" ) {
|
||||
_validationErrors.delete( textName );
|
||||
} else if ( message !== "" ) {
|
||||
_validationErrors.set( textName, message );
|
||||
}
|
||||
|
||||
return _validationErrors;
|
||||
} );
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function onTextInput() {
|
||||
textDirty = true;
|
||||
}
|
||||
|
||||
$: validationError = validateText( text, toggle );
|
||||
|
||||
/**
|
||||
* If appropriate, clicking the header toggles to toggle switch.
|
||||
*/
|
||||
function headingClickHandler() {
|
||||
if ( toggleName && !toggleDisabled ) {
|
||||
toggle = !toggle;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="setting" class:nested class:first>
|
||||
<PanelRow class="option">
|
||||
{#if toggleName}
|
||||
<ToggleSwitch name={toggleName} bind:checked={toggle} disabled={toggleDisabled}>
|
||||
{heading}
|
||||
</ToggleSwitch>
|
||||
<!-- TODO: Fix a11y. -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<h4 id={headingName} on:click={headingClickHandler} class="toggler" class:toggleDisabled>{heading}</h4>
|
||||
{:else}
|
||||
<h4 id={headingName}>{heading}</h4>
|
||||
{/if}
|
||||
<DefinedInWPConfig defined={$definedSettings.includes( toggleName ) || (input && $definedSettings.includes( textName ))}/>
|
||||
</PanelRow>
|
||||
<PanelRow class="desc">
|
||||
<p>{@html description}</p>
|
||||
</PanelRow>
|
||||
{#if input}
|
||||
<PanelRow class="input">
|
||||
<input
|
||||
type="text"
|
||||
id={textName}
|
||||
name={textName}
|
||||
bind:value={text}
|
||||
on:input={onTextInput}
|
||||
minlength="1"
|
||||
size="10"
|
||||
{placeholder}
|
||||
disabled={textDisabled}
|
||||
class:disabled={textDisabled}
|
||||
aria-labelledby={headingName}
|
||||
>
|
||||
<label for={textName}>
|
||||
{heading}
|
||||
</label>
|
||||
</PanelRow>
|
||||
{#if validationError && textDirty}
|
||||
<p class="input-error" transition:slide>{validationError}</p>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if toggleName}
|
||||
<SettingNotifications settingKey={toggleName}/>
|
||||
{/if}
|
||||
|
||||
{#if textName}
|
||||
<SettingNotifications settingKey={textName}/>
|
||||
{/if}
|
||||
|
||||
<slot/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toggler:not(.toggleDisabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
38
ui/components/SettingsValidationStatusRow.svelte
Normal file
38
ui/components/SettingsValidationStatusRow.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script>
|
||||
import {
|
||||
settings_validation,
|
||||
urls
|
||||
} from "../js/stores";
|
||||
import CheckAgain from "./CheckAgain.svelte";
|
||||
|
||||
export let section = "";
|
||||
|
||||
$: success = $settings_validation[ section ].type === "success";
|
||||
$: warning = $settings_validation[ section ].type === "warning";
|
||||
$: error = $settings_validation[ section ].type === "error";
|
||||
$: info = $settings_validation[ section ].type === "info";
|
||||
$: type = $settings_validation[ section ].type;
|
||||
|
||||
$: message = '<p>' + $settings_validation[ section ].message + '</p>';
|
||||
$: iconURL = $urls.assets + "img/icon/notification-" + $settings_validation[ section ].type + ".svg";
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="notification in-panel multiline {section}"
|
||||
class:success
|
||||
class:warning
|
||||
class:error
|
||||
class:info
|
||||
>
|
||||
<div class="content in-panel">
|
||||
<div class="icon type in-panel">
|
||||
<img class="icon type" src={iconURL} alt="{type} icon"/>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
{@html message}
|
||||
</div>
|
||||
|
||||
<CheckAgain section={section}/>
|
||||
</div>
|
||||
</div>
|
||||
52
ui/components/StoragePage.svelte
Normal file
52
ui/components/StoragePage.svelte
Normal file
@@ -0,0 +1,52 @@
|
||||
<script>
|
||||
import {afterUpdate, setContext} from "svelte";
|
||||
import {location, push} from "svelte-spa-router";
|
||||
import {
|
||||
current_settings,
|
||||
settingsLocked,
|
||||
needs_access_keys
|
||||
} from "../js/stores";
|
||||
import Page from "./Page.svelte";
|
||||
import Notifications from "./Notifications.svelte";
|
||||
import SubNav from "./SubNav.svelte";
|
||||
import SubPages from "./SubPages.svelte";
|
||||
import {pages} from "../js/routes";
|
||||
|
||||
export let name = "storage";
|
||||
export let params = {}; // Required for regex routes.
|
||||
const _params = params; // Stops compiler warning about unused params export;
|
||||
|
||||
// During initial setup some storage sub pages behave differently.
|
||||
// Not having a bucket defined is akin to initial setup, but changing provider in sub page may also flip the switch.
|
||||
if ( $current_settings.bucket ) {
|
||||
setContext( "initialSetup", false );
|
||||
} else {
|
||||
setContext( "initialSetup", true );
|
||||
}
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const prefix = "/storage";
|
||||
|
||||
let items = pages.withPrefix( prefix );
|
||||
let routes = pages.routes( prefix );
|
||||
|
||||
afterUpdate( () => {
|
||||
items = pages.withPrefix( prefix );
|
||||
routes = pages.routes( prefix );
|
||||
|
||||
// Ensure only Storage Provider subpage can be visited if credentials not set.
|
||||
if ( $needs_access_keys && $location.startsWith( "/storage/" ) && $location !== "/storage/provider" ) {
|
||||
push( "/storage/provider" );
|
||||
}
|
||||
} );
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" tabParent="media"/>
|
||||
|
||||
<SubNav {name} {items} progress/>
|
||||
|
||||
<SubPages {name} {prefix} {routes} on:routeEvent/>
|
||||
</Page>
|
||||
315
ui/components/StorageProviderSubPage.svelte
Normal file
315
ui/components/StorageProviderSubPage.svelte
Normal file
@@ -0,0 +1,315 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext, hasContext} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
import {
|
||||
settings,
|
||||
defined_settings,
|
||||
strings,
|
||||
storage_providers,
|
||||
storage_provider,
|
||||
counts,
|
||||
current_settings,
|
||||
needs_refresh,
|
||||
revalidatingSettings,
|
||||
state
|
||||
} from "../js/stores";
|
||||
import {
|
||||
scrollNotificationsIntoView
|
||||
} from "../js/scrollNotificationsIntoView";
|
||||
import {needsRefresh} from "../js/needsRefresh";
|
||||
import SubPage from "./SubPage.svelte";
|
||||
import Panel from "./Panel.svelte";
|
||||
import PanelRow from "./PanelRow.svelte";
|
||||
import TabButton from "./TabButton.svelte";
|
||||
import RadioButton from "./RadioButton.svelte";
|
||||
import AccessKeysDefine from "./AccessKeysDefine.svelte";
|
||||
import BackNextButtonsRow from "./BackNextButtonsRow.svelte";
|
||||
import KeyFileDefine from "./KeyFileDefine.svelte";
|
||||
import UseServerRolesDefine from "./UseServerRolesDefine.svelte";
|
||||
import AccessKeysEntry from "./AccessKeysEntry.svelte";
|
||||
import KeyFileEntry from "./KeyFileEntry.svelte";
|
||||
import Notification from "./Notification.svelte";
|
||||
|
||||
export let params = {}; // Required for regex routes.
|
||||
const _params = params; // Stops compiler warning about unused params export;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Parent page may want to be locked.
|
||||
let settingsLocked = writable( false );
|
||||
|
||||
if ( hasContext( "settingsLocked" ) ) {
|
||||
settingsLocked = getContext( "settingsLocked" );
|
||||
}
|
||||
|
||||
// Need to be careful about throwing unneeded warnings.
|
||||
let initialSettings = $current_settings;
|
||||
|
||||
if ( hasContext( "initialSettings" ) ) {
|
||||
initialSettings = getContext( "initialSettings" );
|
||||
}
|
||||
|
||||
// As this page does not directly alter the settings store until done,
|
||||
// we need to keep track of any changes made elsewhere and prompt
|
||||
// the user to refresh the page.
|
||||
let saving = false;
|
||||
const previousSettings = { ...$current_settings };
|
||||
const previousDefines = { ...$defined_settings };
|
||||
|
||||
$: {
|
||||
$needs_refresh = $needs_refresh || needsRefresh( saving, previousSettings, $current_settings, previousDefines, $defined_settings );
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Select Storage Provider
|
||||
*/
|
||||
|
||||
let storageProvider = { ...$storage_provider };
|
||||
|
||||
$: defined = $defined_settings.includes( "provider" );
|
||||
$: disabled = defined || $needs_refresh || $settingsLocked;
|
||||
|
||||
/**
|
||||
* Handles picking different storage provider.
|
||||
*
|
||||
* @param {Object} provider
|
||||
*/
|
||||
function handleChooseProvider( provider ) {
|
||||
if ( disabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
storageProvider = provider;
|
||||
|
||||
// Now make sure authMethod is valid for chosen storage provider.
|
||||
authMethod = getAuthMethod( storageProvider, authMethod );
|
||||
}
|
||||
|
||||
$: changedWithOffloaded = initialSettings.provider !== storageProvider.provider_key_name && $counts.offloaded > 0;
|
||||
|
||||
/*
|
||||
* 2. Select Authentication method
|
||||
*/
|
||||
|
||||
let endpointUrl = $settings[ "endpoint" ] || "";
|
||||
|
||||
let accessKeyId = $settings[ "access-key-id" ];
|
||||
let secretAccessKey = $settings[ "secret-access-key" ];
|
||||
let keyFile = $settings[ "key-file" ] ? JSON.stringify( $settings[ "key-file" ] ) : "";
|
||||
|
||||
/**
|
||||
* For the given current storage provider, determine the authentication method or fallback to currently selected.
|
||||
* It's possible that the storage provider can be freely changed but the
|
||||
* authentication method is defined (fixed) differently for each, or freely changeable too.
|
||||
* The order of evaluation in this function is important and mirrors the server side evaluation order.
|
||||
*
|
||||
* @param {provider} provider
|
||||
* @param {string} current auth method, one of "define", "server-role" or "database" if set.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getAuthMethod( provider, current = "" ) {
|
||||
if ( provider.use_access_keys_allowed && provider.used_access_keys_constants.length ) {
|
||||
return "define";
|
||||
}
|
||||
|
||||
if ( provider.use_key_file_allowed && provider.used_key_file_path_constants.length ) {
|
||||
return "define";
|
||||
}
|
||||
|
||||
if ( provider.use_server_roles_allowed && provider.used_server_roles_constants.length ) {
|
||||
return "server-role";
|
||||
}
|
||||
|
||||
if ( current === "server-role" && !provider.use_server_roles_allowed ) {
|
||||
return "define";
|
||||
}
|
||||
|
||||
if ( current.length === 0 ) {
|
||||
if ( provider.use_access_keys_allowed && (accessKeyId || secretAccessKey) ) {
|
||||
return "database";
|
||||
}
|
||||
|
||||
if ( provider.use_key_file_allowed && keyFile ) {
|
||||
return "database";
|
||||
}
|
||||
|
||||
if ( provider.use_server_roles_allowed && $settings[ "use-server-roles" ] ) {
|
||||
return "server-role";
|
||||
}
|
||||
|
||||
// Default to most secure option.
|
||||
return "define";
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
let authMethod = getAuthMethod( storageProvider );
|
||||
|
||||
// If auth method is not allowed to be database, then either define or server-role is being forced, likely by a define.
|
||||
$: authDefined = "database" !== getAuthMethod( storageProvider, "database" );
|
||||
$: authDisabled = authDefined || $needs_refresh || $settingsLocked;
|
||||
|
||||
/*
|
||||
* 3. Save Authentication Credentials
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a title string to be used for the credentials panel as appropriate for the auth method.
|
||||
*
|
||||
* @param {string} method
|
||||
* @return {*}
|
||||
*/
|
||||
function getCredentialsTitle( method ) {
|
||||
return $strings.auth_method_title[ method ];
|
||||
}
|
||||
|
||||
$: saveCredentialsTitle = getCredentialsTitle( authMethod );
|
||||
|
||||
/*
|
||||
* Do Something!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
saving = true;
|
||||
state.pausePeriodicFetch();
|
||||
|
||||
$settings.provider = storageProvider.provider_key_name;
|
||||
$settings[ "endpoint" ] = storageProvider.provider_key_name === "s3compatible" ? endpointUrl : "";
|
||||
$settings[ "access-key-id" ] = accessKeyId;
|
||||
$settings[ "secret-access-key" ] = secretAccessKey;
|
||||
$settings[ "use-server-roles" ] = authMethod === "server-role";
|
||||
$settings[ "key-file" ] = keyFile;
|
||||
const result = await settings.save();
|
||||
|
||||
// If something went wrong, don't move onto next step.
|
||||
if ( !result.hasOwnProperty( "saved" ) || !result.saved ) {
|
||||
settings.reset();
|
||||
saving = false;
|
||||
await state.resumePeriodicFetch();
|
||||
|
||||
scrollNotificationsIntoView();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$revalidatingSettings = true;
|
||||
const statePromise = state.resumePeriodicFetch();
|
||||
|
||||
dispatch( "routeEvent", { event: "settings.save", data: result } );
|
||||
|
||||
// Just make sure periodic state fetch promise is done with,
|
||||
// even though we don't really care about it.
|
||||
await statePromise;
|
||||
$revalidatingSettings = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<SubPage name="storage-provider-settings" route="/storage/provider">
|
||||
{#if changedWithOffloaded}
|
||||
<Notification inline warning heading={storageProvider.media_already_offloaded_warning.heading}>
|
||||
<p>{@html storageProvider.media_already_offloaded_warning.message}</p>
|
||||
</Notification>
|
||||
{/if}
|
||||
|
||||
<Panel heading={$strings.select_storage_provider_title} defined={defined} multi>
|
||||
<PanelRow class="body flex-row tab-buttons">
|
||||
{#each Object.values( $storage_providers ) as provider}
|
||||
<TabButton
|
||||
active={provider.provider_key_name === storageProvider.provider_key_name}
|
||||
{disabled}
|
||||
icon={provider.icon}
|
||||
iconDesc={provider.icon_desc}
|
||||
text={provider.provider_service_name}
|
||||
on:click={() => handleChooseProvider( provider )}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<Notification class="notice-qsg">
|
||||
<p>{@html storageProvider.get_access_keys_help}</p>
|
||||
</Notification>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
{#if storageProvider.provider_key_name === "s3compatible"}
|
||||
<Panel heading={$strings.s3compat_endpoint_title} multi>
|
||||
<PanelRow class="body flex-column">
|
||||
<label class="input-label" for="s3compat-endpoint">{$strings.s3compat_endpoint_label}</label>
|
||||
<input
|
||||
type="url"
|
||||
id="s3compat-endpoint"
|
||||
name="endpoint"
|
||||
placeholder={$strings.s3compat_endpoint_placeholder}
|
||||
bind:value={endpointUrl}
|
||||
class:disabled
|
||||
{disabled}
|
||||
>
|
||||
<p class="desc">{$strings.s3compat_endpoint_desc}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
||||
<Panel heading={$strings.select_auth_method_title} defined={authDefined} multi>
|
||||
<PanelRow class="body flex-column">
|
||||
<!-- define -->
|
||||
{#if storageProvider.use_access_keys_allowed}
|
||||
<RadioButton bind:selected={authMethod} disabled={authDisabled} value="define" desc={storageProvider.defined_auth_desc}>
|
||||
{$strings.define_access_keys}
|
||||
</RadioButton>
|
||||
{:else if storageProvider.use_key_file_allowed}
|
||||
<RadioButton bind:selected={authMethod} disabled={authDisabled} value="define" desc={storageProvider.defined_auth_desc}>
|
||||
{$strings.define_key_file_path}
|
||||
</RadioButton>
|
||||
{/if}
|
||||
|
||||
<!-- server-role -->
|
||||
{#if storageProvider.use_server_roles_allowed}
|
||||
<RadioButton bind:selected={authMethod} disabled={authDisabled} value="server-role" desc={storageProvider.defined_auth_desc}>
|
||||
{storageProvider.use_server_roles_title}
|
||||
</RadioButton>
|
||||
{/if}
|
||||
|
||||
<!-- database -->
|
||||
{#if storageProvider.use_access_keys_allowed}
|
||||
<RadioButton bind:selected={authMethod} disabled={authDisabled} value="database">
|
||||
{$strings.store_access_keys_in_db}
|
||||
</RadioButton>
|
||||
{:else if storageProvider.use_key_file_allowed}
|
||||
<RadioButton bind:selected={authMethod} disabled={authDisabled} value="database">
|
||||
{$strings.store_key_file_in_db}
|
||||
</RadioButton>
|
||||
{/if}
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
{#if !authDefined}
|
||||
<Panel heading={saveCredentialsTitle} multi>
|
||||
<PanelRow class="body flex-column access-keys">
|
||||
{#if authMethod === "define" && storageProvider.use_access_keys_allowed}
|
||||
<AccessKeysDefine provider={storageProvider}/>
|
||||
{:else if authMethod === "define" && storageProvider.use_key_file_allowed}
|
||||
<KeyFileDefine provider={storageProvider}/>
|
||||
{:else if authMethod === "server-role" && storageProvider.use_server_roles_allowed}
|
||||
<UseServerRolesDefine provider={storageProvider}/>
|
||||
{:else if authMethod === "database" && storageProvider.use_access_keys_allowed}
|
||||
<AccessKeysEntry
|
||||
provider={storageProvider}
|
||||
bind:accessKeyId
|
||||
bind:secretAccessKey
|
||||
disabled={authDisabled}
|
||||
/>
|
||||
{:else if authMethod === "database" && storageProvider.use_key_file_allowed}
|
||||
<KeyFileEntry provider={storageProvider} bind:value={keyFile}/>
|
||||
{/if}
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
||||
<BackNextButtonsRow on:next={handleNext} nextDisabled={$needs_refresh || $settingsLocked} nextText={$strings.save_and_continue}/>
|
||||
</SubPage>
|
||||
76
ui/components/StorageSettingsHeadingRow.svelte
Normal file
76
ui/components/StorageSettingsHeadingRow.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script>
|
||||
import {getContext, hasContext} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
import {push} from "svelte-spa-router";
|
||||
import {
|
||||
region_name,
|
||||
settings,
|
||||
storage_provider,
|
||||
strings,
|
||||
urls
|
||||
} from "../js/stores";
|
||||
import PanelRow from "./PanelRow.svelte";
|
||||
import Button from "./Button.svelte";
|
||||
|
||||
// Parent page may want to be locked.
|
||||
let settingsLocked = writable( false );
|
||||
|
||||
if ( hasContext( "settingsLocked" ) ) {
|
||||
settingsLocked = getContext( "settingsLocked" );
|
||||
}
|
||||
</script>
|
||||
|
||||
<PanelRow header gradient class="storage {$storage_provider.provider_key_name}">
|
||||
<img src="{$storage_provider.icon}" alt={$storage_provider.provider_service_name}/>
|
||||
<div class="provider-details">
|
||||
<h3>{$storage_provider.provider_service_name}</h3>
|
||||
<p class="console-details">
|
||||
<a href={$urls.storage_provider_console_url} class="console" target="_blank" title={$strings.view_provider_console}>{$settings.bucket}</a>
|
||||
<span class="region" title={$settings.region}>{$region_name}</span>
|
||||
</p>
|
||||
</div>
|
||||
<Button outline on:click={() => push('/storage/provider')} title={$strings.edit_storage_provider} disabled={$settingsLocked}>{$strings.edit}</Button>
|
||||
</PanelRow>
|
||||
|
||||
<style>
|
||||
:global(#as3cf-settings.wpome div.panel.settings .header) img {
|
||||
width: var(--as3cf-settings-ctrl-width);
|
||||
height: var(--as3cf-settings-ctrl-width);
|
||||
}
|
||||
|
||||
.provider-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
margin-left: var(--as3cf-settings-option-indent);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:global(#as3cf-settings.wpome div.panel) .provider-details h3 {
|
||||
margin-left: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:global(#as3cf-settings.wpome div.panel) .console-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.console-details .console {
|
||||
flex: 0 1 min-content;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:global(#as3cf-settings.wpome div.panel) .console-details a[target="_blank"].console:after {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:global(#as3cf-settings.wpome div.panel) .console-details .region {
|
||||
flex: 1 0 auto;
|
||||
color: var(--as3cf-color-gray-500);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
</style>
|
||||
48
ui/components/StorageSettingsPanel.svelte
Normal file
48
ui/components/StorageSettingsPanel.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<script>
|
||||
import {settings, strings} from "../js/stores";
|
||||
import Panel from "./Panel.svelte";
|
||||
import StorageSettingsHeadingRow from "./StorageSettingsHeadingRow.svelte";
|
||||
import SettingsValidationStatusRow
|
||||
from "./SettingsValidationStatusRow.svelte";
|
||||
import SettingsPanelOption from "./SettingsPanelOption.svelte";
|
||||
</script>
|
||||
|
||||
<Panel name="settings" heading={$strings.storage_settings_title} helpKey="storage-provider">
|
||||
<StorageSettingsHeadingRow/>
|
||||
<SettingsValidationStatusRow section="storage"/>
|
||||
<SettingsPanelOption
|
||||
heading={$strings.copy_files_to_bucket}
|
||||
description={$strings.copy_files_to_bucket_desc}
|
||||
toggleName="copy-to-s3"
|
||||
bind:toggle={$settings["copy-to-s3"]}
|
||||
/>
|
||||
<SettingsPanelOption
|
||||
heading={$strings.remove_local_file}
|
||||
description={$strings.remove_local_file_desc}
|
||||
toggleName="remove-local-file"
|
||||
bind:toggle={$settings["remove-local-file"]}
|
||||
>
|
||||
</SettingsPanelOption>
|
||||
<SettingsPanelOption
|
||||
heading={$strings.path}
|
||||
description={$strings.path_desc}
|
||||
toggleName="enable-object-prefix"
|
||||
bind:toggle={$settings["enable-object-prefix"]}
|
||||
textName="object-prefix"
|
||||
bind:text={$settings["object-prefix"]}
|
||||
/>
|
||||
<SettingsPanelOption
|
||||
heading={$strings.year_month}
|
||||
description={$strings.year_month_desc}
|
||||
toggleName="use-yearmonth-folders"
|
||||
bind:toggle={$settings["use-yearmonth-folders"]}
|
||||
>
|
||||
</SettingsPanelOption>
|
||||
<SettingsPanelOption
|
||||
heading={$strings.object_versioning}
|
||||
description={$strings.object_versioning_desc}
|
||||
toggleName="object-versioning"
|
||||
bind:toggle={$settings["object-versioning"]}
|
||||
>
|
||||
</SettingsPanelOption>
|
||||
</Panel>
|
||||
8
ui/components/StorageSettingsSubPage.svelte
Normal file
8
ui/components/StorageSettingsSubPage.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script>
|
||||
import SubPage from "./SubPage.svelte";
|
||||
import StorageSettingsPanel from "./StorageSettingsPanel.svelte";
|
||||
</script>
|
||||
|
||||
<SubPage name="storage-settings">
|
||||
<StorageSettingsPanel/>
|
||||
</SubPage>
|
||||
25
ui/components/SubNav.svelte
Normal file
25
ui/components/SubNav.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import {urls} from "../js/stores";
|
||||
import SubNavItem from "./SubNavItem.svelte";
|
||||
|
||||
export let name = "media";
|
||||
export let items = [];
|
||||
export let subpage = false;
|
||||
export let progress = false;
|
||||
|
||||
$: displayItems = items.filter( ( page ) => page.title && (!page.hasOwnProperty( "enabled" ) || page.enabled() === true) );
|
||||
</script>
|
||||
|
||||
{#if displayItems}
|
||||
<ul class="subnav {name}" class:subpage class:progress>
|
||||
{#each displayItems as page, index}
|
||||
<SubNavItem {page}/>
|
||||
<!-- Show a progress indicator after all but the last item. -->
|
||||
{#if progress && index < (displayItems.length - 1)}
|
||||
<li class="step-arrow">
|
||||
<img src="{$urls.assets + 'img/icon/subnav-arrow.svg'}" alt="">
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
46
ui/components/SubNavItem.svelte
Normal file
46
ui/components/SubNavItem.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script>
|
||||
import {link, location} from "svelte-spa-router";
|
||||
import {urls} from "../js/stores";
|
||||
|
||||
export let page;
|
||||
|
||||
let focus = false;
|
||||
let hover = false;
|
||||
|
||||
$: showIcon = typeof page.noticeIcon === "string" && (["warning", "error"].includes( page.noticeIcon ));
|
||||
$: iconUrl = showIcon ? $urls.assets + "img/icon/tab-notifier-" + page.noticeIcon + ".svg" : "";
|
||||
|
||||
</script>
|
||||
|
||||
<li class="subnav-item" class:active={$location === page.route} class:focus class:hover class:has-icon={showIcon}>
|
||||
<a
|
||||
href={page.route}
|
||||
title={page.title()}
|
||||
use:link
|
||||
on:focusin={() => focus = true}
|
||||
on:focusout={() => focus = false}
|
||||
on:mouseenter={() => hover = true}
|
||||
on:mouseleave={() => hover = false}
|
||||
>
|
||||
{page.title()}
|
||||
{#if showIcon}
|
||||
<div class="notice-icon-wrapper notice-icon-{page.noticeIcon}">
|
||||
<img class="notice-icon" src="{iconUrl}" alt="{page.noticeIcon}">
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
.notice-icon-wrapper {
|
||||
display: inline-block;
|
||||
min-width: 1.1875rem;
|
||||
|
||||
}
|
||||
|
||||
.notice-icon {
|
||||
margin-left: 2px;
|
||||
margin-top: -1.5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
10
ui/components/SubPage.svelte
Normal file
10
ui/components/SubPage.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script>
|
||||
import active from "svelte-spa-router/active";
|
||||
|
||||
export let name = "";
|
||||
export let route = "/";
|
||||
</script>
|
||||
|
||||
<div class="{name}" use:active={route}>
|
||||
<slot/>
|
||||
</div>
|
||||
16
ui/components/SubPages.svelte
Normal file
16
ui/components/SubPages.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script>
|
||||
import Router from "svelte-spa-router";
|
||||
|
||||
export let name = "sub";
|
||||
export let prefix = "";
|
||||
export let routes = {};
|
||||
</script>
|
||||
|
||||
{#if routes}
|
||||
<div class="{name}-page wrapper">
|
||||
<Router {routes} {prefix} on:routeEvent/>
|
||||
<slot>
|
||||
<!-- EXTRA CONTENT GOES HERE -->
|
||||
</slot>
|
||||
</div>
|
||||
{/if}
|
||||
50
ui/components/SupportPage.svelte
Normal file
50
ui/components/SupportPage.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script>
|
||||
import {onMount} from "svelte";
|
||||
import {api, config, diagnostics, strings, urls} from "../js/stores";
|
||||
import Page from "./Page.svelte";
|
||||
import Notifications from "./Notifications.svelte";
|
||||
|
||||
export let name = "support";
|
||||
export let title = $strings.support_tab_title;
|
||||
|
||||
onMount( async () => {
|
||||
const json = await api.get( "diagnostics", {} );
|
||||
|
||||
if ( json.hasOwnProperty( "diagnostics" ) ) {
|
||||
$config.diagnostics = json.diagnostics;
|
||||
}
|
||||
} );
|
||||
</script>
|
||||
|
||||
<Page {name} on:routeEvent>
|
||||
<Notifications tab={name}/>
|
||||
{#if title}
|
||||
<h2 class="page-title">{title}</h2>
|
||||
{/if}
|
||||
<div class="support-page wrapper">
|
||||
|
||||
<slot name="header"/>
|
||||
|
||||
<div class="columns">
|
||||
<div class="support-form">
|
||||
<slot name="content">
|
||||
<div class="lite-support">
|
||||
<p>{@html $strings.no_support}</p>
|
||||
<p>{@html $strings.community_support}</p>
|
||||
<p>{@html $strings.upgrade_for_support}</p>
|
||||
<p>{@html $strings.report_a_bug}</p>
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<div class="diagnostic-info">
|
||||
<hr>
|
||||
<h2 class="page-title">{$strings.diagnostic_info_title}</h2>
|
||||
<pre>{$diagnostics}</pre>
|
||||
<a href={$urls.download_diagnostics} class="button btn-md btn-outline">{$strings.download_diagnostics}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot name="footer"/>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
37
ui/components/TabButton.svelte
Normal file
37
ui/components/TabButton.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
import {strings, urls} from "../js/stores";
|
||||
|
||||
export let active = false;
|
||||
export let disabled = false;
|
||||
export let icon = "";
|
||||
export let iconDesc = "";
|
||||
export let text = ""
|
||||
export let url = $urls.settings;
|
||||
</script>
|
||||
|
||||
<a
|
||||
href={url}
|
||||
class="button-tab"
|
||||
class:active
|
||||
class:btn-disabled={disabled}
|
||||
{disabled}
|
||||
on:click|preventDefault
|
||||
>
|
||||
{#if icon}
|
||||
<img
|
||||
src={icon}
|
||||
type="image/svg+xml"
|
||||
alt={iconDesc}
|
||||
>
|
||||
{/if}
|
||||
{#if text}
|
||||
<p>{text}</p>
|
||||
{/if}
|
||||
{#if active}
|
||||
<img
|
||||
class="checkmark"
|
||||
src="{$urls.assets + 'img/icon/licence-checked.svg'}"
|
||||
type="image/svg+xml" alt={$strings.selected_desc}
|
||||
>
|
||||
{/if}
|
||||
</a>
|
||||
17
ui/components/ToggleSwitch.svelte
Normal file
17
ui/components/ToggleSwitch.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
export let name = "";
|
||||
export let checked = false;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<div class="toggle-switch" class:locked={disabled}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={name}
|
||||
bind:checked={checked}
|
||||
{disabled}
|
||||
/>
|
||||
<label class="toggle-label" for={name}>
|
||||
<slot/>
|
||||
</label>
|
||||
</div>
|
||||
14
ui/components/ToolsPage.svelte
Normal file
14
ui/components/ToolsPage.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import {strings} from "../js/stores";
|
||||
import Page from "./Page.svelte";
|
||||
import Notifications from "./Notifications.svelte";
|
||||
import ToolsUpgrade from "./ToolsUpgrade.svelte";
|
||||
|
||||
export let name = "tools";
|
||||
</script>
|
||||
|
||||
<Page {name} on:routeEvent>
|
||||
<Notifications tab={name}/>
|
||||
<h2 class="page-title">{$strings.tools_title}</h2>
|
||||
<ToolsUpgrade/>
|
||||
</Page>
|
||||
38
ui/components/ToolsUpgrade.svelte
Normal file
38
ui/components/ToolsUpgrade.svelte
Normal file
@@ -0,0 +1,38 @@
|
||||
<script>
|
||||
import {strings, urls} from "../js/stores";
|
||||
import Upsell from "./Upsell.svelte";
|
||||
|
||||
let benefits = [
|
||||
{
|
||||
icon: $urls.assets + 'img/icon/offload-remaining.svg',
|
||||
alt: 'offload icon',
|
||||
text: $strings.tools_uppsell_benefits.offload,
|
||||
},
|
||||
{
|
||||
icon: $urls.assets + 'img/icon/download.svg',
|
||||
alt: 'download icon',
|
||||
text: $strings.tools_uppsell_benefits.download,
|
||||
},
|
||||
{
|
||||
icon: $urls.assets + 'img/icon/remove-from-bucket.svg',
|
||||
alt: 'remove from bucket icon',
|
||||
text: $strings.tools_uppsell_benefits.remove_bucket,
|
||||
},
|
||||
{
|
||||
icon: $urls.assets + 'img/icon/remove-from-server.svg',
|
||||
alt: 'remove from server icon',
|
||||
text: $strings.tools_uppsell_benefits.remove_server,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Upsell benefits={benefits}>
|
||||
<div slot="heading">{$strings.tools_upsell_heading}</div>
|
||||
|
||||
<div slot="description">{@html $strings.tools_upsell_description}</div>
|
||||
|
||||
<a slot="call-to-action" href={$urls.upsell_discount_tools} class="button btn-lg btn-primary">
|
||||
<img src={$urls.assets + "img/icon/stars.svg"} alt="stars icon" style="margin-right: 5px;">
|
||||
{$strings.tools_upsell_cta}
|
||||
</a>
|
||||
</Upsell>
|
||||
78
ui/components/Upsell.svelte
Normal file
78
ui/components/Upsell.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<script>
|
||||
import Panel from "../components/Panel.svelte";
|
||||
|
||||
export let benefits;
|
||||
</script>
|
||||
|
||||
<Panel name="upsell" class="upsell-panel">
|
||||
<div class="branding"></div>
|
||||
<div class="content">
|
||||
<div class="heading">
|
||||
<slot name="heading"></slot>
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
<slot name="description"></slot>
|
||||
</div>
|
||||
|
||||
<div class="benefits">
|
||||
{#each benefits as benefit}
|
||||
<li>
|
||||
<img src="{benefit.icon}" alt="{benefit.alt}">
|
||||
<span>{benefit.text}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="call-to-action">
|
||||
<slot name="call-to-action"></slot>
|
||||
<div class="note">
|
||||
<slot name="call-to-action-note"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
padding: 1.875rem 2.25rem 1.5rem 2.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-top: 1rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.125rem;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 1rem;
|
||||
color: rgba(56, 54, 55, 0.7);
|
||||
}
|
||||
|
||||
.benefits {
|
||||
margin-top: 1.7rem;
|
||||
color: rgba(56, 54, 55, 0.7);
|
||||
}
|
||||
|
||||
.benefits li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.benefits img {
|
||||
height: 40px;
|
||||
margin-left: -5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.call-to-action {
|
||||
margin-top: 0.7rem;
|
||||
}
|
||||
|
||||
.call-to-action .note {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
59
ui/components/UrlPreview.svelte
Normal file
59
ui/components/UrlPreview.svelte
Normal file
@@ -0,0 +1,59 @@
|
||||
<script>
|
||||
import {scale} from "svelte/transition";
|
||||
import {api, settings, settings_changed, strings, urls} from "../js/stores";
|
||||
import Panel from "./Panel.svelte";
|
||||
import PanelRow from "./PanelRow.svelte";
|
||||
|
||||
let parts = $urls.url_parts;
|
||||
|
||||
/**
|
||||
* When settings have changed, show their preview URL, otherwise show saved settings version.
|
||||
*
|
||||
* Note: This function **assigns** to the `example` and `parts` variables to defeat the reactive demons!
|
||||
*
|
||||
* @param {Object} urls
|
||||
* @param {boolean} settingsChanged
|
||||
* @param {Object} settings
|
||||
*
|
||||
* @returns boolean
|
||||
*/
|
||||
async function temporaryUrl( urls, settingsChanged, settings ) {
|
||||
if ( settingsChanged ) {
|
||||
const response = await api.post( "url-preview", {
|
||||
"settings": settings
|
||||
} );
|
||||
|
||||
// Use temporary URLs if available.
|
||||
if ( response.hasOwnProperty( "url_parts" ) ) {
|
||||
parts = response.url_parts;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset back to saved URLs.
|
||||
parts = urls.url_parts;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$: isTemporaryUrl = temporaryUrl( $urls, $settings_changed, $settings );
|
||||
</script>
|
||||
|
||||
{#if parts.length > 0}
|
||||
<Panel name="url-preview" heading={$strings.url_preview_title}>
|
||||
<PanelRow class="desc">
|
||||
<p>{$strings.url_preview_desc}</p>
|
||||
</PanelRow>
|
||||
<PanelRow class="body flex-row">
|
||||
<dl>
|
||||
{#each parts as part (part.title)}
|
||||
<div data-key={part.key} transition:scale>
|
||||
<dt>{part.title}</dt>
|
||||
<dd>{part.example}</dd>
|
||||
</div>
|
||||
{/each}
|
||||
</dl>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
{/if}
|
||||
7
ui/components/UseServerRolesDefine.svelte
Normal file
7
ui/components/UseServerRolesDefine.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
export let provider;
|
||||
</script>
|
||||
|
||||
<p>{@html provider.use_server_roles_desc}</p>
|
||||
|
||||
<pre>{provider.use_server_roles_example}</pre>
|
||||
Reference in New Issue
Block a user