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>
|
||||
8
ui/js/autofocus.js
Normal file
8
ui/js/autofocus.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* A simple action that focuses the supplied HTML node.
|
||||
*
|
||||
* @param {Object} node
|
||||
*/
|
||||
export function autofocus( node ) {
|
||||
node.focus();
|
||||
}
|
||||
144
ui/js/defaultPages.js
Normal file
144
ui/js/defaultPages.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import {get} from "svelte/store";
|
||||
import {location} from "svelte-spa-router";
|
||||
import {
|
||||
strings,
|
||||
storage_provider,
|
||||
is_plugin_setup_with_credentials,
|
||||
is_plugin_setup,
|
||||
needs_access_keys,
|
||||
delivery_provider
|
||||
} from "./stores";
|
||||
|
||||
// Components used for default pages.
|
||||
import MediaPage from "../components/MediaPage.svelte";
|
||||
import StoragePage from "../components/StoragePage.svelte";
|
||||
import StorageProviderSubPage
|
||||
from "../components/StorageProviderSubPage.svelte";
|
||||
import BucketSettingsSubPage from "../components/BucketSettingsSubPage.svelte";
|
||||
import SecuritySubPage from "../components/SecuritySubPage.svelte";
|
||||
import DeliveryPage from "../components/DeliveryPage.svelte";
|
||||
|
||||
// Default pages, having a title means inclusion in main tabs.
|
||||
// NOTE: get() only resolves after initialization, hence arrow functions for getting titles.
|
||||
export const defaultPages = [
|
||||
{
|
||||
position: 0,
|
||||
name: "media-library",
|
||||
title: () => get( strings ).media_tab_title,
|
||||
nav: true,
|
||||
route: "/",
|
||||
routeMatcher: /^\/(media\/.*)*$/,
|
||||
component: MediaPage,
|
||||
default: true
|
||||
},
|
||||
{
|
||||
position: 200,
|
||||
name: "storage",
|
||||
route: "/storage/*",
|
||||
component: StoragePage
|
||||
},
|
||||
{
|
||||
position: 210,
|
||||
name: "storage-provider",
|
||||
title: () => get( strings ).storage_provider_tab_title,
|
||||
subNav: true,
|
||||
route: "/storage/provider",
|
||||
component: StorageProviderSubPage,
|
||||
default: true,
|
||||
events: {
|
||||
"page.initial.settings": ( data ) => {
|
||||
// We need Storage Provider credentials for some pages to be useful.
|
||||
if ( data.hasOwnProperty( "location" ) && get( needs_access_keys ) && !get( is_plugin_setup ) ) {
|
||||
for ( const prefix of ["/storage", "/media", "/delivery"] ) {
|
||||
if ( data.location.startsWith( prefix ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return data.location === "/";
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
position: 220,
|
||||
name: "bucket",
|
||||
title: () => get( strings ).bucket_tab_title,
|
||||
subNav: true,
|
||||
route: "/storage/bucket",
|
||||
component: BucketSettingsSubPage,
|
||||
enabled: () => {
|
||||
return !get( needs_access_keys );
|
||||
},
|
||||
events: {
|
||||
"page.initial.settings": ( data ) => {
|
||||
// We need a bucket and region to have been verified before some pages are useful.
|
||||
if ( data.hasOwnProperty( "location" ) && !get( needs_access_keys ) && !get( is_plugin_setup ) ) {
|
||||
for ( const prefix of ["/storage", "/media", "/delivery"] ) {
|
||||
if ( data.location.startsWith( prefix ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return data.location === "/";
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
"settings.save": ( data ) => {
|
||||
// If currently in /storage/provider route, bucket is always next, assuming storage provider set up correctly.
|
||||
return get( location ) === "/storage/provider" && !get( needs_access_keys );
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
position: 230,
|
||||
name: "security",
|
||||
title: () => get( strings ).security_tab_title,
|
||||
subNav: true,
|
||||
route: "/storage/security",
|
||||
component: SecuritySubPage,
|
||||
enabled: () => {
|
||||
return get( is_plugin_setup_with_credentials ) && !get( storage_provider ).requires_acls;
|
||||
},
|
||||
events: {
|
||||
"settings.save": ( data ) => {
|
||||
// If currently in /storage/bucket route,
|
||||
// and storage provider does not require ACLs,
|
||||
// and bucket wasn't just created during initial set up
|
||||
// with delivery provider compatible access control,
|
||||
// then security is next.
|
||||
if (
|
||||
get( location ) === "/storage/bucket" &&
|
||||
get( is_plugin_setup_with_credentials ) &&
|
||||
!get( storage_provider ).requires_acls &&
|
||||
(
|
||||
!data.hasOwnProperty( "bucketSource" ) || // unexpected data issue
|
||||
data.bucketSource !== "new" || // bucket not created
|
||||
!data.hasOwnProperty( "initialSettings" ) || // unexpected data issue
|
||||
!data.initialSettings.hasOwnProperty( "bucket" ) || // unexpected data issue
|
||||
data.initialSettings.bucket.length > 0 || // bucket previously set
|
||||
!data.hasOwnProperty( "settings" ) || // unexpected data issue
|
||||
!data.settings.hasOwnProperty( "use-bucket-acls" ) || // unexpected data issue
|
||||
(
|
||||
!data.settings[ "use-bucket-acls" ] && // bucket not using ACLs ...
|
||||
get( delivery_provider ).requires_acls // ... but delivery provider needs ACLs
|
||||
)
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
position: 300,
|
||||
name: "delivery",
|
||||
route: "/delivery/*",
|
||||
component: DeliveryPage
|
||||
},
|
||||
];
|
||||
12
ui/js/delay.js
Normal file
12
ui/js/delay.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Return a promise that resolves after a minimum amount of time has elapsed.
|
||||
*
|
||||
* @param {number} start Timestamp of when the action started.
|
||||
* @param {number} minTime Minimum amount of time to delay in milliseconds.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
export function delayMin( start, minTime ) {
|
||||
let elapsed = Date.now() - start;
|
||||
return new Promise( ( resolve ) => setTimeout( resolve, minTime - elapsed ) );
|
||||
}
|
||||
8
ui/js/getLocale.js
Normal file
8
ui/js/getLocale.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Get the user's current locale string.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
export function getLocale() {
|
||||
return (navigator.languages && navigator.languages.length) ? navigator.languages[ 0 ] : navigator.language;
|
||||
}
|
||||
24
ui/js/needsRefresh.js
Normal file
24
ui/js/needsRefresh.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import {objectsDiffer} from "./objectsDiffer";
|
||||
|
||||
/**
|
||||
* Determines whether a page should be refreshed due to changes to settings.
|
||||
*
|
||||
* @param {boolean} saving
|
||||
* @param {object} previousSettings
|
||||
* @param {object} currentSettings
|
||||
* @param {object} previousDefines
|
||||
* @param {object} currentDefines
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function needsRefresh( saving, previousSettings, currentSettings, previousDefines, currentDefines ) {
|
||||
if ( saving ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( objectsDiffer( [previousSettings, currentSettings] ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return objectsDiffer( [previousDefines, currentDefines] );
|
||||
}
|
||||
23
ui/js/numToString.js
Normal file
23
ui/js/numToString.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import {getLocale} from "./getLocale";
|
||||
|
||||
/**
|
||||
* Get number formatted for user's current locale.
|
||||
*
|
||||
* @param {number} num
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
export function numToString( num ) {
|
||||
return Intl.NumberFormat( getLocale() ).format( num );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number formatted with short representation for user's current locale.
|
||||
*
|
||||
* @param {number} num
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
export function numToShortString( num ) {
|
||||
return Intl.NumberFormat( getLocale(), { notation: "compact" } ).format( num );
|
||||
}
|
||||
41
ui/js/objectsDiffer.js
Normal file
41
ui/js/objectsDiffer.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Does the current object have different keys or values compared to the previous version?
|
||||
*
|
||||
* @param {object} previous
|
||||
* @param {object} current
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function objectsDiffer( [previous, current] ) {
|
||||
if ( !previous || !current ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Any difference in keys?
|
||||
const prevKeys = Object.keys( previous );
|
||||
const currKeys = Object.keys( current );
|
||||
|
||||
if ( prevKeys.length !== currKeys.length ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Symmetrical diff to find extra keys in either object.
|
||||
if (
|
||||
prevKeys.filter( x => !currKeys.includes( x ) )
|
||||
.concat(
|
||||
currKeys.filter( x => !prevKeys.includes( x ) )
|
||||
)
|
||||
.length > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Any difference in values?
|
||||
for ( const key in previous ) {
|
||||
if ( JSON.stringify( current[ key ] ) !== JSON.stringify( previous[ key ] ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
117
ui/js/routes.js
Normal file
117
ui/js/routes.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import {derived, writable, get} from "svelte/store";
|
||||
import {wrap} from "svelte-spa-router/wrap";
|
||||
|
||||
/**
|
||||
* Creates store of default pages.
|
||||
*
|
||||
* Having a title means inclusion in main tabs.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function createPages() {
|
||||
// NOTE: get() only resolves after initialization, hence arrow functions for getting titles.
|
||||
const { subscribe, set, update } = writable( [] );
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
add( page ) {
|
||||
update( $pages => {
|
||||
return [...$pages, page]
|
||||
.sort( ( a, b ) => {
|
||||
return a.position - b.position;
|
||||
} );
|
||||
} );
|
||||
},
|
||||
withPrefix( prefix = null ) {
|
||||
return get( this ).filter( ( page ) => {
|
||||
return (prefix && page.route.startsWith( prefix )) || !prefix;
|
||||
} );
|
||||
},
|
||||
routes( prefix = null ) {
|
||||
let defaultComponent = null;
|
||||
let defaultUserData = null;
|
||||
const routes = new Map();
|
||||
|
||||
// If a page can be enabled/disabled, check whether it is enabled before displaying.
|
||||
const conditions = [
|
||||
( detail ) => {
|
||||
if (
|
||||
detail.hasOwnProperty( "userData" ) &&
|
||||
detail.userData.hasOwnProperty( "page" ) &&
|
||||
detail.userData.page.hasOwnProperty( "enabled" )
|
||||
) {
|
||||
return detail.userData.page.enabled();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
];
|
||||
|
||||
for ( const page of this.withPrefix( prefix ) ) {
|
||||
const userData = { page: page };
|
||||
|
||||
let route = page.route;
|
||||
|
||||
if ( prefix && route !== prefix + "/*" ) {
|
||||
route = route.replace( prefix, "" );
|
||||
}
|
||||
|
||||
routes.set( route, wrap( {
|
||||
component: page.component,
|
||||
userData: userData,
|
||||
conditions: conditions
|
||||
} ) );
|
||||
|
||||
if ( !defaultComponent && page.default ) {
|
||||
defaultComponent = page.component;
|
||||
defaultUserData = userData;
|
||||
}
|
||||
}
|
||||
|
||||
if ( defaultComponent ) {
|
||||
routes.set( "*", wrap( {
|
||||
component: defaultComponent,
|
||||
userData: defaultUserData,
|
||||
conditions: conditions
|
||||
} ) );
|
||||
}
|
||||
|
||||
return routes;
|
||||
},
|
||||
handleRouteEvent( detail ) {
|
||||
if ( detail.hasOwnProperty( "event" ) ) {
|
||||
if ( !detail.hasOwnProperty( "data" ) ) {
|
||||
detail.data = {};
|
||||
}
|
||||
|
||||
// Find the first page that wants to handle the event
|
||||
// , but also let other pages see the event
|
||||
// so they can set any initial state etc.
|
||||
let route = false;
|
||||
for ( const page of get( this ).values() ) {
|
||||
if ( page.events && page.events[ detail.event ] && page.events[ detail.event ]( detail.data ) && !route ) {
|
||||
route = page.route;
|
||||
}
|
||||
}
|
||||
|
||||
if ( route ) {
|
||||
return route;
|
||||
}
|
||||
}
|
||||
|
||||
if ( detail.hasOwnProperty( "default" ) ) {
|
||||
return detail.default;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const pages = createPages();
|
||||
|
||||
// Convenience readable store of all routes.
|
||||
export const routes = derived( pages, () => {
|
||||
return pages.routes();
|
||||
} );
|
||||
11
ui/js/scrollIntoView.js
Normal file
11
ui/js/scrollIntoView.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* A simple action to scroll the element into view if active.
|
||||
*
|
||||
* @param {Object} node
|
||||
* @param {boolean} active
|
||||
*/
|
||||
export function scrollIntoView( node, active ) {
|
||||
if ( active ) {
|
||||
node.scrollIntoView( { behavior: "smooth", block: "center", inline: "nearest" } );
|
||||
}
|
||||
}
|
||||
10
ui/js/scrollNotificationsIntoView.js
Normal file
10
ui/js/scrollNotificationsIntoView.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Scrolls the notifications into view.
|
||||
*/
|
||||
export function scrollNotificationsIntoView() {
|
||||
const element = document.getElementById( "notifications" );
|
||||
|
||||
if ( element ) {
|
||||
element.scrollIntoView( { behavior: "smooth", block: "start" } );
|
||||
}
|
||||
}
|
||||
51
ui/js/settingsNotifications.js
Normal file
51
ui/js/settingsNotifications.js
Normal file
@@ -0,0 +1,51 @@
|
||||
export const settingsNotifications = {
|
||||
/**
|
||||
* Process local and server settings to return a new Map of inline notifications.
|
||||
*
|
||||
* @param {Map} notifications
|
||||
* @param {Object} settings
|
||||
* @param {Object} current_settings
|
||||
* @param {Object} strings
|
||||
*
|
||||
* @return {Map<string, Map<string, Object>>} keyed by setting name, containing map of notification objects keyed by id.
|
||||
*/
|
||||
process: ( notifications, settings, current_settings, strings ) => {
|
||||
// remove-local-file
|
||||
if ( settings.hasOwnProperty( "remove-local-file" ) && settings[ "remove-local-file" ] ) {
|
||||
let entries = notifications.has( "remove-local-file" ) ? notifications.get( "remove-local-file" ) : new Map();
|
||||
|
||||
if ( settings.hasOwnProperty( "serve-from-s3" ) && !settings[ "serve-from-s3" ] ) {
|
||||
if ( !entries.has( "lost-files-notice" ) ) {
|
||||
entries.set( "lost-files-notice", {
|
||||
inline: true,
|
||||
type: "error",
|
||||
heading: strings.lost_files_notice_heading,
|
||||
message: strings.lost_files_notice_message
|
||||
} );
|
||||
}
|
||||
} else {
|
||||
entries.delete( "lost-files-notice" );
|
||||
}
|
||||
|
||||
// Show inline warning about potential compatibility issues
|
||||
// when turning on setting for the first time.
|
||||
if (
|
||||
!entries.has( "remove-local-file-notice" ) &&
|
||||
current_settings.hasOwnProperty( "remove-local-file" ) &&
|
||||
!current_settings[ "remove-local-file" ]
|
||||
) {
|
||||
entries.set( "remove-local-file-notice", {
|
||||
inline: true,
|
||||
type: "warning",
|
||||
message: strings.remove_local_file_message
|
||||
} );
|
||||
}
|
||||
|
||||
notifications.set( "remove-local-file", entries );
|
||||
} else {
|
||||
notifications.delete( "remove-local-file" );
|
||||
}
|
||||
|
||||
return notifications;
|
||||
}
|
||||
};
|
||||
594
ui/js/stores.js
Normal file
594
ui/js/stores.js
Normal file
@@ -0,0 +1,594 @@
|
||||
import {derived, writable, get, readable} from "svelte/store";
|
||||
import {objectsDiffer} from "./objectsDiffer";
|
||||
|
||||
// Initial config store.
|
||||
export const config = writable( {} );
|
||||
|
||||
// Whether settings are locked due to background activity such as upgrade.
|
||||
export const settingsLocked = writable( false );
|
||||
|
||||
// Convenience readable store of server's settings, derived from config.
|
||||
export const current_settings = derived( config, $config => $config.settings );
|
||||
|
||||
// Convenience readable store of defined settings keys, derived from config.
|
||||
export const defined_settings = derived( config, $config => $config.defined_settings );
|
||||
|
||||
// Convenience readable store of translated strings, derived from config.
|
||||
export const strings = derived( config, $config => $config.strings );
|
||||
|
||||
// Convenience readable store for nonce, derived from config.
|
||||
export const nonce = derived( config, $config => $config.nonce );
|
||||
|
||||
// Convenience readable store of urls, derived from config.
|
||||
export const urls = derived( config, $config => $config.urls );
|
||||
|
||||
// Convenience readable store of docs, derived from config.
|
||||
export const docs = derived( config, $config => $config.docs );
|
||||
|
||||
// Convenience readable store of api endpoints, derived from config.
|
||||
export const endpoints = derived( config, $config => $config.endpoints );
|
||||
|
||||
// Convenience readable store of diagnostics, derived from config.
|
||||
export const diagnostics = derived( config, $config => $config.diagnostics );
|
||||
|
||||
// Convenience readable store of counts, derived from config.
|
||||
export const counts = derived( config, $config => $config.counts );
|
||||
|
||||
// Convenience readable store of summary counts, derived from config.
|
||||
export const summaryCounts = derived( config, $config => $config.summary_counts );
|
||||
|
||||
// Convenience readable store of offload remaining upsell, derived from config.
|
||||
export const offloadRemainingUpsell = derived( config, $config => $config.offload_remaining_upsell );
|
||||
|
||||
// Convenience readable store of upgrades, derived from config.
|
||||
export const upgrades = derived( config, $config => $config.upgrades );
|
||||
|
||||
// Convenience readable store of whether plugin is set up, derived from config.
|
||||
export const is_plugin_setup = derived( config, $config => $config.is_plugin_setup );
|
||||
|
||||
// Convenience readable store of whether plugin is set up, including with credentials, derived from config.
|
||||
export const is_plugin_setup_with_credentials = derived( config, $config => $config.is_plugin_setup_with_credentials );
|
||||
|
||||
// Convenience readable store of whether storage provider needs access credentials, derived from config.
|
||||
export const needs_access_keys = derived( config, $config => $config.needs_access_keys );
|
||||
|
||||
// Convenience readable store of whether bucket is writable, derived from config.
|
||||
export const bucket_writable = derived( config, $config => $config.bucket_writable );
|
||||
|
||||
// Convenience readable store of settings validation results, derived from config.
|
||||
export const settings_validation = derived( config, $config => $config.settings_validation );
|
||||
|
||||
// Store of inline errors and warnings to be shown next to settings.
|
||||
// Format is a map using settings key for keys, values are an array of objects that can be used to instantiate a notification.
|
||||
export const settings_notifications = writable( new Map() );
|
||||
|
||||
// Store of validation errors for settings.
|
||||
// Format is a map using settings key for keys, values are strings containing validation error.
|
||||
export const validationErrors = writable( new Map() );
|
||||
|
||||
// Whether settings validations are being run.
|
||||
export const revalidatingSettings = writable( false );
|
||||
|
||||
// Does the app need a page refresh to resolve conflicts?
|
||||
export const needs_refresh = writable( false );
|
||||
|
||||
// Various stores may call the API, and the api object uses some stores.
|
||||
// To avoid cyclic dependencies, we therefore co-locate the api object with the stores.
|
||||
// We also need to add its functions much later so that JSHint does not complain about using the stores too early.
|
||||
export const api = {};
|
||||
|
||||
/**
|
||||
* Creates store of settings.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function createSettings() {
|
||||
const { subscribe, set, update } = writable( [] );
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
async save() {
|
||||
const json = await api.put( "settings", get( this ) );
|
||||
|
||||
if ( json.hasOwnProperty( "saved" ) && true === json.saved ) {
|
||||
// Sync settings with what the server has.
|
||||
this.updateSettings( json );
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
return { 'saved': false };
|
||||
},
|
||||
reset() {
|
||||
set( { ...get( current_settings ) } );
|
||||
},
|
||||
async fetch() {
|
||||
const json = await api.get( "settings", {} );
|
||||
this.updateSettings( json );
|
||||
},
|
||||
updateSettings( json ) {
|
||||
if (
|
||||
json.hasOwnProperty( "defined_settings" ) &&
|
||||
json.hasOwnProperty( "settings" ) &&
|
||||
json.hasOwnProperty( "storage_providers" ) &&
|
||||
json.hasOwnProperty( "delivery_providers" ) &&
|
||||
json.hasOwnProperty( "is_plugin_setup" ) &&
|
||||
json.hasOwnProperty( "is_plugin_setup_with_credentials" ) &&
|
||||
json.hasOwnProperty( "needs_access_keys" ) &&
|
||||
json.hasOwnProperty( "bucket_writable" ) &&
|
||||
json.hasOwnProperty( "urls" )
|
||||
) {
|
||||
// Update our understanding of what the server's settings are.
|
||||
config.update( $config => {
|
||||
return {
|
||||
...$config,
|
||||
defined_settings: json.defined_settings,
|
||||
settings: json.settings,
|
||||
storage_providers: json.storage_providers,
|
||||
delivery_providers: json.delivery_providers,
|
||||
is_plugin_setup: json.is_plugin_setup,
|
||||
is_plugin_setup_with_credentials: json.is_plugin_setup_with_credentials,
|
||||
needs_access_keys: json.needs_access_keys,
|
||||
bucket_writable: json.bucket_writable,
|
||||
urls: json.urls
|
||||
};
|
||||
} );
|
||||
// Update our local working copy of the settings.
|
||||
update( $settings => {
|
||||
return { ...json.settings };
|
||||
} );
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const settings = createSettings();
|
||||
|
||||
// Have the settings been changed from current server side settings?
|
||||
export const settings_changed = derived( [settings, current_settings], objectsDiffer );
|
||||
|
||||
// Convenience readable store of default storage provider, derived from config.
|
||||
export const defaultStorageProvider = derived( config, $config => $config.default_storage_provider );
|
||||
|
||||
// Convenience readable store of available storage providers.
|
||||
export const storage_providers = derived( [config, urls], ( [$config, $urls] ) => {
|
||||
for ( const key in $config.storage_providers ) {
|
||||
$config.storage_providers[ key ].icon = $urls.assets + "img/icon/provider/storage/" + $config.storage_providers[ key ].provider_key_name + ".svg";
|
||||
$config.storage_providers[ key ].link_icon = $urls.assets + "img/icon/provider/storage/" + $config.storage_providers[ key ].provider_key_name + "-link.svg";
|
||||
$config.storage_providers[ key ].round_icon = $urls.assets + "img/icon/provider/storage/" + $config.storage_providers[ key ].provider_key_name + "-round.svg";
|
||||
}
|
||||
|
||||
return $config.storage_providers;
|
||||
} );
|
||||
|
||||
// Convenience readable store of storage provider's details.
|
||||
export const storage_provider = derived( [settings, storage_providers], ( [$settings, $storage_providers] ) => {
|
||||
if ( $settings.hasOwnProperty( "provider" ) && $storage_providers.hasOwnProperty( $settings.provider ) ) {
|
||||
return $storage_providers[ $settings.provider ];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} );
|
||||
|
||||
// Convenience readable store of default delivery provider, derived from config.
|
||||
export const defaultDeliveryProvider = derived( config, $config => $config.default_delivery_provider );
|
||||
|
||||
// Convenience readable store of available delivery providers.
|
||||
export const delivery_providers = derived( [config, urls, storage_provider], ( [$config, $urls, $storage_provider] ) => {
|
||||
for ( const key in $config.delivery_providers ) {
|
||||
if ( "storage" === key ) {
|
||||
$config.delivery_providers[ key ].icon = $storage_provider.icon;
|
||||
$config.delivery_providers[ key ].round_icon = $storage_provider.round_icon;
|
||||
$config.delivery_providers[ key ].provider_service_quick_start_url = $storage_provider.provider_service_quick_start_url;
|
||||
} else {
|
||||
$config.delivery_providers[ key ].icon = $urls.assets + "img/icon/provider/delivery/" + $config.delivery_providers[ key ].provider_key_name + ".svg";
|
||||
$config.delivery_providers[ key ].round_icon = $urls.assets + "img/icon/provider/delivery/" + $config.delivery_providers[ key ].provider_key_name + "-round.svg";
|
||||
}
|
||||
}
|
||||
|
||||
return $config.delivery_providers;
|
||||
} );
|
||||
|
||||
// Convenience readable store of delivery provider's details.
|
||||
export const delivery_provider = derived( [settings, delivery_providers, urls], ( [$settings, $delivery_providers, $urls] ) => {
|
||||
if ( $settings.hasOwnProperty( "delivery-provider" ) && $delivery_providers.hasOwnProperty( $settings[ "delivery-provider" ] ) ) {
|
||||
return $delivery_providers[ $settings[ "delivery-provider" ] ];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} );
|
||||
|
||||
// Full name for current region.
|
||||
export const region_name = derived( [settings, storage_provider, strings], ( [$settings, $storage_provider, $strings] ) => {
|
||||
if ( $settings.region && $storage_provider.regions && $storage_provider.regions.hasOwnProperty( $settings.region ) ) {
|
||||
return $storage_provider.regions[ $settings.region ];
|
||||
} else if ( $settings.region && $storage_provider.regions ) {
|
||||
// Region set but not available in list of regions.
|
||||
return $strings.unknown;
|
||||
} else if ( $storage_provider.default_region && $storage_provider.regions && $storage_provider.regions.hasOwnProperty( $storage_provider.default_region ) ) {
|
||||
// Region not set but default available.
|
||||
return $storage_provider.regions[ $storage_provider.default_region ];
|
||||
} else {
|
||||
// Possibly no default region or regions available.
|
||||
return $strings.unknown;
|
||||
}
|
||||
} );
|
||||
|
||||
// Convenience readable store of whether Block All Public Access is enabled.
|
||||
export const bapa = derived( [settings, storage_provider], ( [$settings, $storage_provider] ) => {
|
||||
return $storage_provider.block_public_access_supported && $settings.hasOwnProperty( "block-public-access" ) && $settings[ "block-public-access" ];
|
||||
} );
|
||||
|
||||
// Convenience readable store of whether Object Ownership is enforced.
|
||||
export const ooe = derived( [settings, storage_provider], ( [$settings, $storage_provider] ) => {
|
||||
return $storage_provider.object_ownership_supported && $settings.hasOwnProperty( "object-ownership-enforced" ) && $settings[ "object-ownership-enforced" ];
|
||||
} );
|
||||
|
||||
/**
|
||||
* Creates a store of notifications.
|
||||
*
|
||||
* Example object in the array:
|
||||
* {
|
||||
* id: "error-message",
|
||||
* type: "error", // error | warning | success | primary (default)
|
||||
* dismissible: true,
|
||||
* flash: true, // Optional, means notification is context specific and will not persist on server, defaults to true.
|
||||
* inline: false, // Optional, unlikely to be true, included here for completeness.
|
||||
* only_show_on_tab: "media-library", // Optional, blank/missing means on all tabs.
|
||||
* heading: "Global Error: Something has gone terribly pear shaped.", // Optional.
|
||||
* message: "We're so sorry, but unfortunately we're going to have to delete the year 2020.", // Optional.
|
||||
* icon: "notification-error.svg", // Optional icon file name to be shown in front of heading.
|
||||
* plainHeading: false, // Optional boolean as to whether a <p> tag should be used instead of <h3> for heading content.
|
||||
* extra: "", // Optional extra content to be shown in paragraph below message.
|
||||
* links: [], // Optional list of links to be shown at bottom of notice.
|
||||
* },
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function createNotifications() {
|
||||
const { subscribe, set, update } = writable( [] );
|
||||
|
||||
return {
|
||||
set,
|
||||
subscribe,
|
||||
add( notification ) {
|
||||
// There's a slight difference between our notification's formatting and what WP uses.
|
||||
if ( notification.hasOwnProperty( "type" ) && notification.type === "updated" ) {
|
||||
notification.type = "success";
|
||||
}
|
||||
if ( notification.hasOwnProperty( "type" ) && notification.type === "notice-warning" ) {
|
||||
notification.type = "warning";
|
||||
}
|
||||
if ( notification.hasOwnProperty( "type" ) && notification.type === "notice-info" ) {
|
||||
notification.type = "info";
|
||||
}
|
||||
if (
|
||||
notification.hasOwnProperty( "message" ) &&
|
||||
(!notification.hasOwnProperty( "heading" ) || notification.heading.trim().length === 0)
|
||||
) {
|
||||
notification.heading = notification.message;
|
||||
notification.plainHeading = true;
|
||||
delete notification.message;
|
||||
}
|
||||
if ( !notification.hasOwnProperty( "flash" ) ) {
|
||||
notification.flash = true;
|
||||
}
|
||||
|
||||
// We need some sort of id for indexing and to ensure rendering is efficient.
|
||||
if ( !notification.hasOwnProperty( "id" ) ) {
|
||||
// Notifications are useless without at least a heading or message, so we can be sure at least one exists.
|
||||
const idHeading = notification.hasOwnProperty( "heading" ) ? notification.heading.trim() : "dynamic-heading";
|
||||
const idMessage = notification.hasOwnProperty( "message" ) ? notification.message.trim() : "dynamic-message";
|
||||
|
||||
notification.id = btoa( idHeading + idMessage );
|
||||
}
|
||||
|
||||
// So that rendering is efficient, but updates displayed notifications that re-use keys,
|
||||
// we create a render_key based on id and created_at as created_at is churned on re-use.
|
||||
const createdAt = notification.hasOwnProperty( "created_at" ) ? notification.created_at : 0;
|
||||
notification.render_key = notification.id + "-" + createdAt;
|
||||
|
||||
update( $notifications => {
|
||||
// Maybe update a notification if id already exists.
|
||||
let index = -1;
|
||||
if ( notification.hasOwnProperty( "id" ) ) {
|
||||
index = $notifications.findIndex( _notification => _notification.id === notification.id );
|
||||
}
|
||||
|
||||
if ( index >= 0 ) {
|
||||
// If the id exists but has been dismissed, add the replacement notification to the end of the array
|
||||
// if given notification is newer, otherwise skip it entirely.
|
||||
if ( $notifications[ index ].hasOwnProperty( "dismissed" ) ) {
|
||||
if ( $notifications[ index ].dismissed < notification.created_at ) {
|
||||
$notifications.push( notification );
|
||||
$notifications.splice( index, 1 );
|
||||
}
|
||||
} else {
|
||||
// Update existing.
|
||||
$notifications.splice( index, 1, notification );
|
||||
}
|
||||
} else {
|
||||
// Add new.
|
||||
$notifications.push( notification );
|
||||
}
|
||||
|
||||
return $notifications.sort( this.sortCompare );
|
||||
} );
|
||||
},
|
||||
sortCompare( a, b ) {
|
||||
// Sort by created_at in case an existing notification was updated.
|
||||
if ( a.created_at < b.created_at ) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if ( a.created_at > b.created_at ) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
async dismiss( id ) {
|
||||
update( $notifications => {
|
||||
const index = $notifications.findIndex( notification => notification.id === id );
|
||||
|
||||
// If the notification still exists, set a "dismissed" tombstone with the created_at value.
|
||||
// The cleanup will delete any notifications that have been dismissed and no longer exist
|
||||
// in the list of notifications retrieved from the server.
|
||||
// The created_at value ensures that if a notification is retrieved from the server that
|
||||
// has the same id but later created_at, then it can be added, otherwise it is skipped.
|
||||
if ( index >= 0 ) {
|
||||
if ( $notifications[ index ].hasOwnProperty( "created_at" ) ) {
|
||||
$notifications[ index ].dismissed = $notifications[ index ].created_at;
|
||||
} else {
|
||||
// Notification likely did not come from server, maybe a local "flash" notification.
|
||||
$notifications.splice( index, 1 );
|
||||
}
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
} );
|
||||
|
||||
// Tell server to dismiss notification, still ok to try if flash notification, makes sure it is definitely removed.
|
||||
await api.delete( "notifications", { id: id, all_tabs: true } );
|
||||
},
|
||||
/**
|
||||
* Delete removes a notification from the UI without telling the server.
|
||||
*/
|
||||
delete( id ) {
|
||||
update( $notifications => {
|
||||
const index = $notifications.findIndex( notification => notification.id === id );
|
||||
|
||||
if ( index >= 0 ) {
|
||||
$notifications.splice( index, 1 );
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
} );
|
||||
},
|
||||
cleanup( latest ) {
|
||||
update( $notifications => {
|
||||
for ( const [index, notification] of $notifications.entries() ) {
|
||||
// Only clean up dismissed or server created notices that no longer exist.
|
||||
if ( notification.hasOwnProperty( "dismissed" ) || notification.hasOwnProperty( "created_at" ) ) {
|
||||
const latestIndex = latest.findIndex( _notification => _notification.id === notification.id );
|
||||
|
||||
// If server doesn't know about the notification anymore, remove it.
|
||||
if ( latestIndex < 0 ) {
|
||||
$notifications.splice( index, 1 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
} );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const notifications = createNotifications();
|
||||
|
||||
// Controller for periodic fetch of state info.
|
||||
let stateFetchInterval;
|
||||
let stateFetchIntervalStarted = false;
|
||||
let stateFetchIntervalPaused = false;
|
||||
|
||||
// Store of functions to call before an update of state processes the result into config.
|
||||
export const preStateUpdateCallbacks = writable( [] );
|
||||
|
||||
// Store of functions to call after an update of state processes the result into config.
|
||||
export const postStateUpdateCallbacks = writable( [] );
|
||||
|
||||
/**
|
||||
* Store of functions to call when state info is updated, and actual API access methods.
|
||||
*
|
||||
* Functions are called after the returned state info has been used to update the config store.
|
||||
* Therefore, functions should only be added to the store if extra processing is required.
|
||||
* The functions should be asynchronous as they are part of the reactive chain and called with await.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function createState() {
|
||||
const { subscribe, set, update } = writable( [] );
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
update,
|
||||
async fetch() {
|
||||
const json = await api.get( "state", {} );
|
||||
|
||||
// Abort controller is still a bit hit or miss, so we'll go old skool.
|
||||
if ( stateFetchIntervalStarted && !stateFetchIntervalPaused ) {
|
||||
this.updateState( json );
|
||||
}
|
||||
},
|
||||
updateState( json ) {
|
||||
for ( const callable of get( preStateUpdateCallbacks ) ) {
|
||||
callable( json );
|
||||
}
|
||||
|
||||
const dirty = get( settings_changed );
|
||||
const previous_settings = { ...get( current_settings ) }; // cloned
|
||||
|
||||
config.update( $config => {
|
||||
return { ...$config, ...json };
|
||||
} );
|
||||
|
||||
// If the settings weren't changed before, they shouldn't be now.
|
||||
if ( !dirty && get( settings_changed ) ) {
|
||||
settings.reset();
|
||||
}
|
||||
|
||||
// If settings are in middle of being changed when changes come in
|
||||
// from server, reset to server version.
|
||||
if ( dirty && objectsDiffer( [previous_settings, get( current_settings )] ) ) {
|
||||
needs_refresh.update( $needs_refresh => true );
|
||||
settings.reset();
|
||||
}
|
||||
|
||||
for ( const callable of get( postStateUpdateCallbacks ) ) {
|
||||
callable( json );
|
||||
}
|
||||
},
|
||||
async startPeriodicFetch() {
|
||||
stateFetchIntervalStarted = true;
|
||||
stateFetchIntervalPaused = false;
|
||||
|
||||
await this.fetch();
|
||||
|
||||
stateFetchInterval = setInterval( async () => {
|
||||
await this.fetch();
|
||||
}, 5000 );
|
||||
},
|
||||
stopPeriodicFetch() {
|
||||
stateFetchIntervalStarted = false;
|
||||
stateFetchIntervalPaused = false;
|
||||
|
||||
clearInterval( stateFetchInterval );
|
||||
},
|
||||
pausePeriodicFetch() {
|
||||
if ( stateFetchIntervalStarted ) {
|
||||
stateFetchIntervalPaused = true;
|
||||
clearInterval( stateFetchInterval );
|
||||
}
|
||||
},
|
||||
async resumePeriodicFetch() {
|
||||
stateFetchIntervalPaused = false;
|
||||
|
||||
if ( stateFetchIntervalStarted ) {
|
||||
await this.startPeriodicFetch();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const state = createState();
|
||||
|
||||
// API functions added here to avoid JSHint errors.
|
||||
api.headers = () => {
|
||||
return {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': get( nonce )
|
||||
};
|
||||
};
|
||||
|
||||
api.url = ( endpoint ) => {
|
||||
return get( urls ).api + get( endpoints )[ endpoint ];
|
||||
};
|
||||
|
||||
api.get = async ( endpoint, params ) => {
|
||||
let url = new URL( api.url( endpoint ) );
|
||||
|
||||
const searchParams = new URLSearchParams( params );
|
||||
|
||||
searchParams.forEach( function( value, name ) {
|
||||
url.searchParams.set( name, value );
|
||||
} );
|
||||
|
||||
const response = await fetch( url.toString(), {
|
||||
method: 'GET',
|
||||
headers: api.headers()
|
||||
} );
|
||||
return response.json().then( json => {
|
||||
json = api.check_response( json );
|
||||
return json;
|
||||
} );
|
||||
};
|
||||
|
||||
api.post = async ( endpoint, body ) => {
|
||||
const response = await fetch( api.url( endpoint ), {
|
||||
method: 'POST',
|
||||
headers: api.headers(),
|
||||
body: JSON.stringify( body )
|
||||
} );
|
||||
return response.json().then( json => {
|
||||
json = api.check_response( json );
|
||||
return json;
|
||||
} );
|
||||
};
|
||||
|
||||
api.put = async ( endpoint, body ) => {
|
||||
const response = await fetch( api.url( endpoint ), {
|
||||
method: 'PUT',
|
||||
headers: api.headers(),
|
||||
body: JSON.stringify( body )
|
||||
} );
|
||||
return response.json().then( json => {
|
||||
json = api.check_response( json );
|
||||
return json;
|
||||
} );
|
||||
};
|
||||
|
||||
api.delete = async ( endpoint, body ) => {
|
||||
const response = await fetch( api.url( endpoint ), {
|
||||
method: 'DELETE',
|
||||
headers: api.headers(),
|
||||
body: JSON.stringify( body )
|
||||
} );
|
||||
return response.json().then( json => {
|
||||
json = api.check_response( json );
|
||||
return json;
|
||||
} );
|
||||
};
|
||||
|
||||
api.check_errors = ( json ) => {
|
||||
if ( json.code && json.message ) {
|
||||
notifications.add( {
|
||||
id: json.code,
|
||||
type: 'error',
|
||||
dismissible: true,
|
||||
heading: get( strings ).api_error_notice_heading,
|
||||
message: json.message
|
||||
} );
|
||||
|
||||
// Just in case resultant json is expanded into a store.
|
||||
delete json.code;
|
||||
delete json.message;
|
||||
}
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
api.check_notifications = ( json ) => {
|
||||
const _notifications = json.hasOwnProperty( "notifications" ) ? json.notifications : [];
|
||||
if ( _notifications ) {
|
||||
for ( const notification of _notifications ) {
|
||||
notifications.add( notification );
|
||||
}
|
||||
}
|
||||
notifications.cleanup( _notifications );
|
||||
|
||||
// Just in case resultant json is expanded into a store.
|
||||
delete json.notifications;
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
api.check_response = ( json ) => {
|
||||
json = api.check_notifications( json );
|
||||
json = api.check_errors( json );
|
||||
|
||||
return json;
|
||||
};
|
||||
33
ui/pro/AssetsPage.svelte
Normal file
33
ui/pro/AssetsPage.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script>
|
||||
import {
|
||||
assetsSettings,
|
||||
assetsSettingsChanged,
|
||||
assetsSettingsLocked,
|
||||
currentAssetsSettings,
|
||||
enableAssets
|
||||
} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import AssetsSettings from "./AssetsSettings.svelte";
|
||||
import AssetsUpgrade from "./AssetsUpgrade.svelte";
|
||||
import Footer from "../components/Footer.svelte";
|
||||
import {setContext} from "svelte";
|
||||
|
||||
export let name = "assets";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", assetsSettingsLocked );
|
||||
</script>
|
||||
|
||||
<Page {name} on:routeEvent initialSettings={currentAssetsSettings}>
|
||||
<Notifications tab={name}/>
|
||||
<div class="assets-page wrapper">
|
||||
{#if $enableAssets}
|
||||
<AssetsSettings/>
|
||||
{:else}
|
||||
<AssetsUpgrade/>
|
||||
{/if}
|
||||
</div>
|
||||
</Page>
|
||||
|
||||
<Footer settingsStore={assetsSettings} settingsChangedStore={assetsSettingsChanged} on:routeEvent/>
|
||||
67
ui/pro/AssetsSettings.svelte
Normal file
67
ui/pro/AssetsSettings.svelte
Normal file
@@ -0,0 +1,67 @@
|
||||
<script>
|
||||
import {strings, urls} from "../js/stores";
|
||||
import {assetsSettings, assetsDefinedSettings} from "./stores";
|
||||
import AssetsSettingsHeaderRow from "./AssetsSettingsHeaderRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import SettingsPanelOption from "../components/SettingsPanelOption.svelte";
|
||||
import SettingsValidationStatusRow
|
||||
from "../components/SettingsValidationStatusRow.svelte";
|
||||
|
||||
/**
|
||||
* Potentially returns a reason that the provided domain name is invalid.
|
||||
*
|
||||
* @param {string} domain
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function validator( 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;
|
||||
} else if ( domain === $urls.home_domain ) {
|
||||
message = $strings.assets_domain_same_as_site;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel name="settings" class="assets-panel" heading={$strings.assets_title} helpKey="assets-pull">
|
||||
<AssetsSettingsHeaderRow/>
|
||||
<SettingsValidationStatusRow section="assets"/>
|
||||
<SettingsPanelOption
|
||||
heading={$strings.assets_rewrite_urls}
|
||||
description={$strings.assets_rewrite_urls_desc}
|
||||
placeholder="assets.example.com"
|
||||
toggleName="rewrite-urls"
|
||||
bind:toggle={$assetsSettings["rewrite-urls"]}
|
||||
textName="domain"
|
||||
bind:text={$assetsSettings["domain"]}
|
||||
definedSettings={assetsDefinedSettings}
|
||||
{validator}
|
||||
>
|
||||
</SettingsPanelOption>
|
||||
|
||||
<SettingsPanelOption
|
||||
heading={$strings.assets_force_https}
|
||||
description={$strings.assets_force_https_desc}
|
||||
toggleName="force-https"
|
||||
bind:toggle={$assetsSettings["force-https"]}
|
||||
definedSettings={assetsDefinedSettings}
|
||||
/>
|
||||
</Panel>
|
||||
|
||||
<!--
|
||||
<div class="btn-row">
|
||||
<div class="notice">
|
||||
<img class="icon notice-icon assets-wizard" src="{$urls.assets + 'img/icon/assets-wizard.svg'}" alt="Launch the Assets Setup Wizard"/><a href={$urls.settings} class="link">Launch the Assets Setup Wizard</a>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
42
ui/pro/AssetsSettingsHeaderRow.svelte
Normal file
42
ui/pro/AssetsSettingsHeaderRow.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<script>
|
||||
import {strings, urls} from "../js/stores";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
</script>
|
||||
|
||||
|
||||
<PanelRow header class="assets">
|
||||
<img src="{$urls.assets + 'img/icon/assets.svg'}" alt="foo"/>
|
||||
<div class="assets-details">
|
||||
<h3>{$strings.assets_panel_header}</h3>
|
||||
<p class="console-details">
|
||||
{$strings.assets_panel_header_details}
|
||||
</p>
|
||||
</div>
|
||||
</PanelRow>
|
||||
|
||||
<style>
|
||||
:global(#as3cf-settings.wpome div.panel.settings .header) img {
|
||||
width: var(--as3cf-settings-ctrl-width);
|
||||
height: var(--as3cf-settings-ctrl-width);
|
||||
}
|
||||
|
||||
.assets-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
margin-left: var(--as3cf-settings-option-indent);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:global(#as3cf-settings.wpome div.panel) .assets-details h3 {
|
||||
margin-left: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:global(#as3cf-settings.wpome div.panel) .console-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--as3cf-color-gray-600);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
37
ui/pro/AssetsUpgrade.svelte
Normal file
37
ui/pro/AssetsUpgrade.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
import {strings, urls} from "../js/stores";
|
||||
import Upsell from "../components/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>
|
||||
|
||||
<div slot="call-to-action-note">
|
||||
{@html $strings.assets_upsell_cta_note}
|
||||
</div>
|
||||
</Upsell>
|
||||
62
ui/pro/CopyBucketsPromptSubPage.svelte
Normal file
62
ui/pro/CopyBucketsPromptSubPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext, hasContext} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
import {pop} from "svelte-spa-router";
|
||||
import {strings} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import SubPage from "../components/SubPage.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
|
||||
const tool = $tools.copy_buckets;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Parent page may want to be locked.
|
||||
let settingsLocked = writable( false );
|
||||
|
||||
if ( hasContext( "settingsLocked" ) ) {
|
||||
settingsLocked = getContext( "settingsLocked" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<SubPage name="copy-buckets" route="/storage/copy-buckets">
|
||||
<Panel
|
||||
heading={tool.title}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
skipText={$strings.no}
|
||||
nextText={$strings.yes}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</SubPage>
|
||||
13
ui/pro/DocumentationSidebar.svelte
Normal file
13
ui/pro/DocumentationSidebar.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
import {strings} from "../js/stores";
|
||||
import {documentation} from "./stores";
|
||||
</script>
|
||||
|
||||
{#if $documentation.length}
|
||||
<div class="documentation">
|
||||
<h3>{$strings.documentation_title}</h3>
|
||||
{#each $documentation as item}
|
||||
<a href={item.url} class="link" target="_blank">{item.title}</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
62
ui/pro/DownloaderPromptPage.svelte
Normal file
62
ui/pro/DownloaderPromptPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {settingsLocked} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
|
||||
export let name = "downloader";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const tool = $tools.downloader;
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" component={ToolNotification}/>
|
||||
|
||||
<Panel
|
||||
heading={tool.name}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
nextText={tool.button}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</Page>
|
||||
26
ui/pro/Header.svelte
Normal file
26
ui/pro/Header.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script>
|
||||
import {push} from "svelte-spa-router";
|
||||
import {strings, urls} from "../js/stores";
|
||||
import {licence} from "./stores";
|
||||
import Header from "../components/Header.svelte";
|
||||
import Button from "../components/Button.svelte";
|
||||
</script>
|
||||
|
||||
<Header>
|
||||
{#if $licence.is_set}
|
||||
{#if $licence.is_valid}
|
||||
<div class="licence-type">
|
||||
<img src={$urls.assets + "img/icon/licence-checked.svg"} alt={$strings.licence_checked}/>
|
||||
<a href={$urls.licenses} class="licence" target="_blank">{$licence.plan_plus_licence}</a>
|
||||
</div>
|
||||
<p>{@html $licence.customer}</p>
|
||||
{:else}
|
||||
<div class="licence-type">
|
||||
<img src={$urls.assets + "img/icon/error.svg"} alt={$strings.licence_error}/>
|
||||
<a href={$urls.licenses} class="licence" target="_blank">{$licence.status_description}</a>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Button large primary on:click={() => push("/license")}>{$strings.activate_licence}</Button>
|
||||
{/if}
|
||||
</Header>
|
||||
103
ui/pro/LicencePage.svelte
Normal file
103
ui/pro/LicencePage.svelte
Normal file
@@ -0,0 +1,103 @@
|
||||
<script>
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import {api, config, settings, strings} from "../js/stores";
|
||||
import {autofocus} from "../js/autofocus";
|
||||
import {licence} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import Button from "../components/Button.svelte";
|
||||
import DefinedInWPConfig from "../components/DefinedInWPConfig.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let name = "licence";
|
||||
|
||||
let value = "";
|
||||
|
||||
/**
|
||||
* Handles an "Activate License" button click.
|
||||
*
|
||||
* @param {Object} event
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleActivateLicence( event ) {
|
||||
const result = await api.post( "licences", { licence: value } );
|
||||
|
||||
await updateLicenceInfo( result )
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a "Remove License" button click.
|
||||
*
|
||||
* @param {Object} event
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleRemoveLicence( event ) {
|
||||
value = "";
|
||||
const result = await api.delete( "licences" );
|
||||
|
||||
await updateLicenceInfo( result )
|
||||
}
|
||||
|
||||
/**
|
||||
* Update licence store with results of API call.
|
||||
*
|
||||
* @param {Object} response
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function updateLicenceInfo( response ) {
|
||||
if ( response.hasOwnProperty( "licences" ) ) {
|
||||
config.update( currentConfig => {
|
||||
return {
|
||||
...currentConfig,
|
||||
licences: response.licences
|
||||
};
|
||||
} );
|
||||
}
|
||||
|
||||
// Regardless of what just happened, make sure our settings are in sync (includes reference to license).
|
||||
await settings.fetch();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} on:routeEvent>
|
||||
<Notifications tab={name}/>
|
||||
<h2 class="page-title">{$strings.licence_title}</h2>
|
||||
|
||||
<div class="licence-page wrapper" class:defined={$licence.is_set && $licence.is_defined}>
|
||||
{#if $licence.is_set}
|
||||
<label for="licence-key" class="screen-reader-text">{$strings.licence_title}</label>
|
||||
<input
|
||||
id="licence-key"
|
||||
type="text"
|
||||
class="licence-field disabled"
|
||||
name="licence"
|
||||
value={$licence.masked_licence}
|
||||
disabled
|
||||
>
|
||||
{#if $licence.is_defined}
|
||||
<DefinedInWPConfig defined/>
|
||||
{:else}
|
||||
<Button large outline on:click={handleRemoveLicence}>{$strings.remove_licence}</Button>
|
||||
{/if}
|
||||
{:else}
|
||||
<label for="enter-licence-key" class="screen-reader-text">{$strings.enter_licence_key}</label>
|
||||
<input
|
||||
id="enter-licence-key"
|
||||
type="text"
|
||||
class="licence-field"
|
||||
name="licence"
|
||||
minlength="4"
|
||||
placeholder={$strings.enter_licence_key}
|
||||
bind:value
|
||||
use:autofocus
|
||||
>
|
||||
<Button large primary on:click={handleActivateLicence} disabled={value.length === 0}>
|
||||
{$strings.activate_licence}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Page>
|
||||
92
ui/pro/MoveObjectsPromptPage.svelte
Normal file
92
ui/pro/MoveObjectsPromptPage.svelte
Normal file
@@ -0,0 +1,92 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {settingsLocked} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
|
||||
export let name = "move-objects";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const moveObjectsTool = $tools.move_objects;
|
||||
const movePublicObjectsTool = $tools.move_public_objects;
|
||||
const movePrivateObjectsTool = $tools.move_private_objects;
|
||||
|
||||
let movePublicObjects = false;
|
||||
let movePrivateObjects = true;
|
||||
|
||||
$: nextDisabled = $settingsLocked || (!movePublicObjects && !movePrivateObjects);
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
let tool = moveObjectsTool;
|
||||
|
||||
if ( !movePublicObjects || !movePrivateObjects ) {
|
||||
tool = movePublicObjects ? movePublicObjectsTool : movePrivateObjectsTool;
|
||||
}
|
||||
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" component={ToolNotification}/>
|
||||
|
||||
<Panel
|
||||
class="toggle-header"
|
||||
heading={movePublicObjectsTool.name}
|
||||
toggleName="move-public-objects"
|
||||
bind:toggle={movePublicObjects}
|
||||
helpURL={movePublicObjectsTool.doc_url}
|
||||
helpDesc={movePublicObjectsTool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html movePublicObjectsTool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
class="toggle-header"
|
||||
heading={movePrivateObjectsTool.name}
|
||||
toggleName="move-private-objects"
|
||||
bind:toggle={movePrivateObjects}
|
||||
helpURL={movePrivateObjectsTool.doc_url}
|
||||
helpDesc={movePrivateObjectsTool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html movePrivateObjectsTool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
nextText={moveObjectsTool.button}
|
||||
skipVisible={true}
|
||||
{nextDisabled}
|
||||
/>
|
||||
</Page>
|
||||
62
ui/pro/MovePrivateObjectsPromptPage.svelte
Normal file
62
ui/pro/MovePrivateObjectsPromptPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {settingsLocked} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
|
||||
export let name = "move-private-objects";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const tool = $tools.move_private_objects;
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" component={ToolNotification}/>
|
||||
|
||||
<Panel
|
||||
heading={tool.name}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
nextText={tool.button}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</Page>
|
||||
62
ui/pro/MovePublicObjectsPromptPage.svelte
Normal file
62
ui/pro/MovePublicObjectsPromptPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {settingsLocked} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
|
||||
export let name = "move-public-objects";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const tool = $tools.move_public_objects;
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" component={ToolNotification}/>
|
||||
|
||||
<Panel
|
||||
heading={tool.name}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
nextText={tool.button}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</Page>
|
||||
109
ui/pro/Nav.svelte
Normal file
109
ui/pro/Nav.svelte
Normal file
@@ -0,0 +1,109 @@
|
||||
<script>
|
||||
import {link} from "svelte-spa-router";
|
||||
import {bucket_writable, counts, strings, urls} from "../js/stores";
|
||||
import {licence, offloadRemainingWithCount, running, tools} from "./stores";
|
||||
import Nav from "../components/Nav.svelte";
|
||||
import OffloadStatus from "../components/OffloadStatus.svelte";
|
||||
import ToolRunningStatus from "./ToolRunningStatus.svelte";
|
||||
import OffloadStatusFlyout from "../components/OffloadStatusFlyout.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
import Button from "../components/Button.svelte";
|
||||
|
||||
let flyoutButton;
|
||||
let expanded = false;
|
||||
let hasFocus = false;
|
||||
|
||||
/**
|
||||
* Get a message describing why the offload remaining button is disabled, if it is.
|
||||
*
|
||||
* @param {Object} licence
|
||||
* @param {Object} counts
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getOffloadRemainingDisabledMessage( licence, counts ) {
|
||||
if ( !licence.is_set ) {
|
||||
return $strings.no_licence;
|
||||
}
|
||||
|
||||
if ( counts.total < 1 ) {
|
||||
return $strings.no_media;
|
||||
}
|
||||
|
||||
if ( counts.not_offloaded < 1 ) {
|
||||
return $strings.all_media_offloaded;
|
||||
}
|
||||
|
||||
if (
|
||||
licence.limit_info.counts_toward_limit &&
|
||||
licence.limit_info.total > 0 &&
|
||||
licence.limit_info.limit > 0 &&
|
||||
licence.limit_info.total >= licence.limit_info.limit
|
||||
) {
|
||||
if ( licence.limit_info.total > licence.limit_info.limit ) {
|
||||
return $strings.licence_limit_exceeded;
|
||||
}
|
||||
|
||||
return $strings.licence_limit_reached;
|
||||
}
|
||||
|
||||
if ( ! $bucket_writable ) {
|
||||
return $strings.disabled_tool_bucket_access;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
$: offloadRemainingDisabledMessage = getOffloadRemainingDisabledMessage( $licence, $counts );
|
||||
|
||||
/**
|
||||
* Close the flyout panel and kick off the offloader.
|
||||
*
|
||||
* The panel is closed so that it does not pop back open without focus on completion.
|
||||
*/
|
||||
function startOffload() {
|
||||
expanded = false;
|
||||
tools.start( $tools.uploader );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Nav>
|
||||
{#if !!$running}
|
||||
<ToolRunningStatus/>
|
||||
{:else}
|
||||
<OffloadStatus bind:flyoutButton bind:expanded bind:hasFocus>
|
||||
<svelte:fragment slot="flyout">
|
||||
<OffloadStatusFlyout bind:expanded bind:hasFocus bind:buttonRef={flyoutButton}>
|
||||
<svelte:fragment slot="footer">
|
||||
<PanelRow footer class="offload-remaining">
|
||||
<Button
|
||||
primary
|
||||
disabled={offloadRemainingDisabledMessage}
|
||||
title={offloadRemainingDisabledMessage}
|
||||
on:click={startOffload}
|
||||
>
|
||||
{$offloadRemainingWithCount}
|
||||
</Button>
|
||||
</PanelRow>
|
||||
|
||||
<PanelRow footer class="licence">
|
||||
<div class="details">
|
||||
<p class="title">{$strings.plan_usage_title}</p>
|
||||
<p>{$licence.plan_usage}</p>
|
||||
</div>
|
||||
{#if !$licence.is_set}
|
||||
<a href="/license" use:link>
|
||||
{$strings.activate_licence}
|
||||
</a>
|
||||
{:else if $licence.limit_info.limit !== 0}
|
||||
<a href={$urls.licenses} target="_blank" class="upgrade">
|
||||
{$strings.upgrade_plan_cta}
|
||||
</a>
|
||||
{/if}
|
||||
</PanelRow>
|
||||
</svelte:fragment>
|
||||
</OffloadStatusFlyout>
|
||||
</svelte:fragment>
|
||||
</OffloadStatus>
|
||||
{/if}
|
||||
</Nav>
|
||||
33
ui/pro/NoTools.svelte
Normal file
33
ui/pro/NoTools.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script>
|
||||
import {strings, urls} from "../js/stores";
|
||||
import Upsell from "../components/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}>
|
||||
<div slot="heading">{$strings.no_tools_header}</div>
|
||||
|
||||
<div slot="description">{@html $strings.no_tools_description}</div>
|
||||
</Upsell>
|
||||
62
ui/pro/RemoveLocalFilesPromptPage.svelte
Normal file
62
ui/pro/RemoveLocalFilesPromptPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {settingsLocked} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
|
||||
export let name = "remove-local-files";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const tool = $tools.remove_local_files;
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" component={ToolNotification}/>
|
||||
|
||||
<Panel
|
||||
heading={tool.name}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
nextText={tool.button}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</Page>
|
||||
201
ui/pro/Settings.svelte
Normal file
201
ui/pro/Settings.svelte
Normal file
@@ -0,0 +1,201 @@
|
||||
<script>
|
||||
import {onMount} from "svelte";
|
||||
import {
|
||||
strings,
|
||||
config,
|
||||
defaultStorageProvider,
|
||||
settingsLocked,
|
||||
notifications,
|
||||
current_settings,
|
||||
needs_access_keys,
|
||||
needs_refresh,
|
||||
counts,
|
||||
settings_notifications,
|
||||
settings,
|
||||
settings_changed,
|
||||
preStateUpdateCallbacks,
|
||||
postStateUpdateCallbacks
|
||||
} from "../js/stores";
|
||||
import {
|
||||
licence,
|
||||
running,
|
||||
tools,
|
||||
toolsLocked,
|
||||
assetsNeedsRefresh,
|
||||
assetsSettingsLocked,
|
||||
assetsSettings,
|
||||
assetsSettingsChanged
|
||||
} from "./stores";
|
||||
import {pages} from "../js/routes";
|
||||
import {defaultPages} from "../js/defaultPages";
|
||||
import {addPages} from "./pages";
|
||||
import {settingsNotifications} from "../js/settingsNotifications";
|
||||
import {toolSettingsNotifications} from "./toolSettingsNotifications";
|
||||
import Settings from "../components/Settings.svelte";
|
||||
import Header from "./Header.svelte";
|
||||
import Nav from "./Nav.svelte";
|
||||
import Pages from "../components/Pages.svelte";
|
||||
|
||||
export let init = {};
|
||||
|
||||
// During initialization set config store to passed in values to avoid undefined values in components during mount.
|
||||
// This saves having to do a lot of checking of values before use.
|
||||
config.set( init );
|
||||
pages.set( defaultPages );
|
||||
|
||||
// We need a disassociated copy of the initial tools info to start with.
|
||||
tools.updateTools( { tools: { ...$config.tools } } );
|
||||
|
||||
// We need a disassociated copy of the initial assets settings to work with.
|
||||
assetsSettings.set( { ...$config.assets_settings } );
|
||||
|
||||
// Add Pro specific pages.
|
||||
addPages( $tools );
|
||||
|
||||
/**
|
||||
* Handles state update event's changes to config.
|
||||
*
|
||||
* @param {Object} config
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleStateUpdate( config ) {
|
||||
let _settingsLocked = false;
|
||||
let _toolsLocked = false;
|
||||
let _assetsSettingsLocked = false;
|
||||
|
||||
// All settings need to be locked?
|
||||
if ( config.upgrades.is_upgrading ) {
|
||||
_settingsLocked = true;
|
||||
_toolsLocked = true;
|
||||
_assetsSettingsLocked = true;
|
||||
|
||||
const notification = {
|
||||
id: "as3cf-all-settings-locked",
|
||||
type: "warning",
|
||||
dismissible: false,
|
||||
heading: config.upgrades.locked_notifications[ config.upgrades.running_upgrade ],
|
||||
icon: "notification-locked.svg",
|
||||
plainHeading: true
|
||||
};
|
||||
notifications.add( notification );
|
||||
|
||||
if ( $settings_changed ) {
|
||||
settings.reset();
|
||||
}
|
||||
|
||||
if ( $assetsSettingsChanged ) {
|
||||
assetsSettings.reset();
|
||||
}
|
||||
} else {
|
||||
notifications.delete( "as3cf-all-settings-locked" );
|
||||
}
|
||||
|
||||
// Media settings need to be locked?
|
||||
if ( $needs_refresh ) {
|
||||
_settingsLocked = true;
|
||||
_toolsLocked = true;
|
||||
|
||||
const notification = {
|
||||
id: "as3cf-media-settings-locked",
|
||||
type: "warning",
|
||||
dismissible: false,
|
||||
only_show_on_tab: "media",
|
||||
heading: $strings.needs_refresh,
|
||||
icon: "notification-locked.svg",
|
||||
plainHeading: true
|
||||
};
|
||||
notifications.add( notification );
|
||||
} else if ( $running ) {
|
||||
_settingsLocked = true;
|
||||
|
||||
const tool = $tools[ $running ];
|
||||
const notification = {
|
||||
id: "as3cf-media-settings-locked",
|
||||
type: "warning",
|
||||
dismissible: false,
|
||||
only_show_on_tab: "media",
|
||||
heading: tool.locked_notification,
|
||||
icon: "notification-locked.svg",
|
||||
plainHeading: true
|
||||
};
|
||||
notifications.add( notification );
|
||||
|
||||
if ( $settings_changed ) {
|
||||
settings.reset();
|
||||
}
|
||||
} else {
|
||||
notifications.delete( "as3cf-media-settings-locked" );
|
||||
}
|
||||
|
||||
// Assets settings need to be locked?
|
||||
if ( $assetsNeedsRefresh ) {
|
||||
_assetsSettingsLocked = true;
|
||||
|
||||
const notification = {
|
||||
id: "as3cf-assets-settings-locked",
|
||||
type: "warning",
|
||||
dismissible: false,
|
||||
only_show_on_tab: "assets",
|
||||
heading: $strings.needs_refresh,
|
||||
icon: "notification-locked.svg",
|
||||
plainHeading: true
|
||||
};
|
||||
notifications.add( notification );
|
||||
} else {
|
||||
notifications.delete( "as3cf-assets-settings-locked" );
|
||||
}
|
||||
|
||||
$settingsLocked = _settingsLocked;
|
||||
$toolsLocked = _toolsLocked;
|
||||
$assetsSettingsLocked = _assetsSettingsLocked;
|
||||
|
||||
// Show a persistent error notice if bucket can't be accessed.
|
||||
if ( $needs_access_keys && ($settings.provider !== $defaultStorageProvider || $settings.bucket.length !== 0) ) {
|
||||
const notification = {
|
||||
id: "as3cf-needs-access-keys",
|
||||
type: "error",
|
||||
dismissible: false,
|
||||
only_show_on_tab: "media",
|
||||
hide_on_parent: true,
|
||||
heading: $strings.needs_access_keys,
|
||||
plainHeading: true
|
||||
};
|
||||
notifications.add( notification );
|
||||
} else {
|
||||
notifications.delete( "as3cf-needs-access-keys" );
|
||||
}
|
||||
}
|
||||
|
||||
// Catch changes to running tool as soon as possible.
|
||||
$: if ( $running ) {
|
||||
handleStateUpdate( $config );
|
||||
}
|
||||
|
||||
// Catch changes to needing access credentials as soon as possible.
|
||||
$: if ( $needs_access_keys ) {
|
||||
handleStateUpdate( $config );
|
||||
}
|
||||
|
||||
onMount( () => {
|
||||
// Make sure state dependent data is up-to-date.
|
||||
handleStateUpdate( $config );
|
||||
|
||||
// When state info is fetched we need some extra processing of the data.
|
||||
preStateUpdateCallbacks.update( _callables => {
|
||||
return [..._callables, assetsSettings.updateSettings];
|
||||
} );
|
||||
|
||||
postStateUpdateCallbacks.update( _callables => {
|
||||
return [..._callables, tools.updateTools, handleStateUpdate];
|
||||
} );
|
||||
} );
|
||||
|
||||
// Make sure all inline notifications are in place.
|
||||
$: settings_notifications.update( ( notices ) => settingsNotifications.process( notices, $settings, $current_settings, $strings ) );
|
||||
$: settings_notifications.update( ( notices ) => toolSettingsNotifications.process( notices, $settings, $current_settings, $strings, $counts, $licence ) );
|
||||
</script>
|
||||
|
||||
<Settings header={Header}>
|
||||
<Pages nav={Nav}/>
|
||||
</Settings>
|
||||
139
ui/pro/SupportForm.svelte
Normal file
139
ui/pro/SupportForm.svelte
Normal file
@@ -0,0 +1,139 @@
|
||||
<script>
|
||||
import {diagnostics, notifications, strings} from "../js/stores";
|
||||
import {licence} from "./stores";
|
||||
import Button from "../components/Button.svelte";
|
||||
|
||||
let email = "";
|
||||
let subject = "";
|
||||
let message = "";
|
||||
let includeDiagnostics = true;
|
||||
|
||||
/**
|
||||
* Potentially returns a reason that the Submit button is disabled.
|
||||
*
|
||||
* @param {string} email
|
||||
* @param {string} subject
|
||||
* @param {string} message
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getDisabledReason( email, subject, message ) {
|
||||
let reason = "";
|
||||
|
||||
if ( !email || !subject || !message ) {
|
||||
reason = "Email, Subject and Message required.";
|
||||
}
|
||||
|
||||
return reason;
|
||||
}
|
||||
|
||||
$: disabledReason = getDisabledReason( email, subject, message );
|
||||
|
||||
/**
|
||||
* Handles a Submit button click.
|
||||
*
|
||||
* @param {Object} event
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function submitSupportRequest( event ) {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append( "email", email );
|
||||
formData.append( "subject", subject );
|
||||
formData.append( "message", message );
|
||||
|
||||
if ( includeDiagnostics ) {
|
||||
formData.append( "local-diagnostic", "1" );
|
||||
formData.append( "local-diagnostic-content", $diagnostics );
|
||||
}
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await fetch(
|
||||
$licence.support_url,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
} catch ( error ) {
|
||||
const notice = $strings.send_email_post_error + error.message;
|
||||
|
||||
notifications.add( {
|
||||
id: "support-send-email-response",
|
||||
type: "error",
|
||||
dismissible: true,
|
||||
only_show_on_tab: "support",
|
||||
message: notice
|
||||
} );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
if ( json.hasOwnProperty( "errors" ) ) {
|
||||
for ( const [key, value] of Object.entries( json.errors ) ) {
|
||||
const notice = $strings.send_email_api_error + value;
|
||||
|
||||
notifications.add( {
|
||||
id: "support-send-email-response",
|
||||
type: "error",
|
||||
dismissible: true,
|
||||
only_show_on_tab: "support",
|
||||
message: notice
|
||||
} );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( json.hasOwnProperty( "success" ) && json.success === 1 ) {
|
||||
notifications.add( {
|
||||
id: "support-send-email-response",
|
||||
type: "success",
|
||||
dismissible: true,
|
||||
only_show_on_tab: "support",
|
||||
message: $strings.send_email_success
|
||||
} );
|
||||
|
||||
email = "";
|
||||
subject = "";
|
||||
message = "";
|
||||
includeDiagnostics = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.add( {
|
||||
id: "support-send-email-response",
|
||||
type: "error",
|
||||
dismissible: true,
|
||||
only_show_on_tab: "support",
|
||||
message: $strings.send_email_unexpected_error
|
||||
} );
|
||||
}
|
||||
</script>
|
||||
|
||||
<label for="email" class="input-label">From</label>
|
||||
<select name="email" id="email" bind:value={email}>
|
||||
{#each $licence.support_email_addresses as supportEmail}
|
||||
<option value={supportEmail}>{supportEmail}</option>
|
||||
{/each}
|
||||
<option value="">{$strings.select_email}</option>
|
||||
</select>
|
||||
<p class="note">{@html $strings.email_note}</p>
|
||||
<input type="text" id="subject" name="subject" bind:value={subject} minlength="4" placeholder={$strings.email_subject_placeholder}>
|
||||
<textarea id="message" name="message" bind:value={message} rows="8" placeholder={$strings.email_message_placeholder}></textarea>
|
||||
<div class="actions">
|
||||
<div class="checkbox">
|
||||
<label for="include-diagnostics">
|
||||
<input type="checkbox" id="include-diagnostics" name="include-diagnostics" bind:checked={includeDiagnostics}>{$strings.attach_diagnostics}
|
||||
</label>
|
||||
</div>
|
||||
<Button primary on:click={submitSupportRequest} disabled={disabledReason} title={disabledReason}>{$strings.send_email}</Button>
|
||||
</div>
|
||||
<p class="note first">{$strings.having_trouble}</p>
|
||||
<p class="note">{@html $strings.email_instead}</p>
|
||||
76
ui/pro/SupportPage.svelte
Normal file
76
ui/pro/SupportPage.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script>
|
||||
import {link} from "svelte-spa-router";
|
||||
import {strings} from "../js/stores";
|
||||
import {licence} from "./stores";
|
||||
import SupportPage from "../components/SupportPage.svelte";
|
||||
import DocumentationSidebar from "./DocumentationSidebar.svelte";
|
||||
import SupportForm from "./SupportForm.svelte";
|
||||
import Notification from "../components/Notification.svelte";
|
||||
|
||||
export let name = "support";
|
||||
|
||||
/**
|
||||
* Potentially returns an error message detailing a problem with the currently set license key.
|
||||
*
|
||||
* @param {Object} licence
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getLicenceError( licence ) {
|
||||
// If there are any errors, just return the first (there's usually only 1 anyway).
|
||||
if ( licence.hasOwnProperty( "errors" ) && Object.values( licence.errors ).length > 0 ) {
|
||||
return Object.values( licence.errors )[ 0 ];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
$: licenceError = getLicenceError( $licence );
|
||||
</script>
|
||||
|
||||
{#if $licence.is_set}
|
||||
{#if $licence.is_valid && licenceError.length === 0}
|
||||
<SupportPage {name} title={$strings.email_support_title} on:routeEvent>
|
||||
<p class="licence-type" slot="header">{@html $licence.your_active_licence}</p>
|
||||
<svelte:fragment slot="content">
|
||||
<SupportForm/>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<DocumentationSidebar/>
|
||||
</svelte:fragment>
|
||||
</SupportPage>
|
||||
{:else}
|
||||
<SupportPage {name} title={$strings.email_support_title} on:routeEvent>
|
||||
<svelte:fragment slot="content">
|
||||
<Notification warning inline>
|
||||
<p>
|
||||
{@html licenceError}
|
||||
</p>
|
||||
</Notification>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<DocumentationSidebar/>
|
||||
</svelte:fragment>
|
||||
</SupportPage>
|
||||
{/if}
|
||||
{:else}
|
||||
<SupportPage {name} title={$strings.email_support_title} on:routeEvent>
|
||||
<svelte:fragment slot="content">
|
||||
<Notification warning inline>
|
||||
<p>
|
||||
{$strings.licence_not_entered}
|
||||
<a href="/license" use:link>
|
||||
{$strings.please_enter_licence}
|
||||
</a>
|
||||
</p>
|
||||
<p>{$strings.once_licence_entered}</p>
|
||||
</Notification>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<DocumentationSidebar/>
|
||||
</svelte:fragment>
|
||||
</SupportPage>
|
||||
{/if}
|
||||
76
ui/pro/ToolNotification.svelte
Normal file
76
ui/pro/ToolNotification.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script>
|
||||
import {fade, slide} from "svelte/transition";
|
||||
import {api, strings} from "../js/stores";
|
||||
import Notification from "../components/Notification.svelte";
|
||||
|
||||
export let notification;
|
||||
|
||||
let expanded = true;
|
||||
|
||||
/**
|
||||
* Handles Dismiss All Errors for item click.
|
||||
*
|
||||
* @param {string} tool_key
|
||||
* @param {Object} item
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function dismissAll( tool_key, item ) {
|
||||
await api.delete( "tools", {
|
||||
id: tool_key,
|
||||
blog_id: item.blog_id,
|
||||
source_type: item.source_type,
|
||||
source_id: item.source_id
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Dismiss Individual Error for item click.
|
||||
*
|
||||
* @param {string} tool_key
|
||||
* @param {Object} item
|
||||
* @param {number} index
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function dismissError( tool_key, item, index ) {
|
||||
await api.delete( "tools", {
|
||||
id: tool_key,
|
||||
blog_id: item.blog_id,
|
||||
source_type: item.source_type,
|
||||
source_id: item.source_id,
|
||||
errors: index
|
||||
} );
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if notification.hasOwnProperty( "class" ) && notification.class === "tool-error" && notification.hasOwnProperty( "errors" )}
|
||||
<Notification notification={notification} expandable bind:expanded>
|
||||
<svelte:fragment slot="details">
|
||||
{#if expanded}
|
||||
<div class="details" transition:slide>
|
||||
{#each notification.errors.details as item, index}
|
||||
<div class="item" transition:fade>
|
||||
<div class="summary">
|
||||
<div class="title">
|
||||
{(index + 1) + ". " + item.source_type_name}
|
||||
<a href={item.edit_url.url}>#{item.source_id}</a>
|
||||
</div>
|
||||
<button class="dismiss" on:click|preventDefault={() => dismissAll(notification.errors.tool_key, item)}>{$strings.dismiss}</button>
|
||||
</div>
|
||||
<ul class="detail">
|
||||
{#each item.messages as message, index}
|
||||
<li>{@html message}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Notification>
|
||||
{:else}
|
||||
<Notification notification={notification}>
|
||||
<slot/>
|
||||
</Notification>
|
||||
{/if}
|
||||
178
ui/pro/ToolPanel.svelte
Normal file
178
ui/pro/ToolPanel.svelte
Normal file
@@ -0,0 +1,178 @@
|
||||
<script>
|
||||
import {bucket_writable, strings, urls} from "../js/stores";
|
||||
import {running, tools, toolsLocked} from "./stores";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
import Button from "../components/Button.svelte";
|
||||
import ProgressBar from "../components/ProgressBar.svelte";
|
||||
import ToolRunningButtons from "./ToolRunningButtons.svelte";
|
||||
import {numToString} from "../js/numToString";
|
||||
|
||||
export let tool = {};
|
||||
|
||||
// Total processed related variables.
|
||||
$: showTotal = !!tool.hasOwnProperty( "total_progress" );
|
||||
$: initial = !!(showTotal && tool.total_progress < 1);
|
||||
$: partialComplete = !!(showTotal && tool.total_progress > 0 && tool.total_progress < 100);
|
||||
$: complete = !!(showTotal && !initial && !partialComplete);
|
||||
|
||||
// In progress related variables.
|
||||
$: isRunning = !!($running && $running === tool.id);
|
||||
$: starting = !!(isRunning && tool.progress < 1 && !tool.is_paused);
|
||||
// Buttons should be disabled if another tool is running, current tool is in process of pausing or cancelling, or all tools locked.
|
||||
$: disabled = ($running && $running !== tool.id) || (tool.is_processing && tool.is_paused) || tool.is_cancelled || $toolsLocked;
|
||||
$: disabled_bucket_access = tool.requires_bucket_access && !$bucket_writable;
|
||||
|
||||
/**
|
||||
* Returns the numeric percentage progress for the running job.
|
||||
*
|
||||
* @param {Object} tool
|
||||
* @param {boolean} isRunning
|
||||
* @param {boolean} showTotal
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
function getPercentComplete( tool, isRunning, showTotal ) {
|
||||
if ( isRunning ) {
|
||||
return tool.progress;
|
||||
} else if ( showTotal ) {
|
||||
return tool.total_progress;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$: percentComplete = getPercentComplete( tool, isRunning, showTotal );
|
||||
|
||||
/**
|
||||
* Returns state dependent icon for tool.
|
||||
*
|
||||
* @param {Object} tool
|
||||
* @param {boolean} isRunning
|
||||
* @return {string}
|
||||
*/
|
||||
function getIcon( tool, isRunning ) {
|
||||
const icon = tools.icon( tool, isRunning, false );
|
||||
|
||||
return $urls.assets + "img/icon/" + icon;
|
||||
}
|
||||
|
||||
$: icon = getIcon( tool, isRunning );
|
||||
|
||||
/**
|
||||
* Potentially returns a map of tools that are related to the current tool.
|
||||
*
|
||||
* Map is keyed by tool's id (key), values are tool objects.
|
||||
*
|
||||
* @param {Object} tool
|
||||
*
|
||||
* @return {Map<string, object>}
|
||||
*/
|
||||
function getRelatedTools( tool ) {
|
||||
let related = new Map();
|
||||
|
||||
if ( tool.hasOwnProperty( "related_tools" ) && tool.related_tools.length > 0 ) {
|
||||
tool.related_tools.forEach( ( key ) => {
|
||||
if ( $tools.hasOwnProperty( key ) ) {
|
||||
related.set( key, $tools[ key ] );
|
||||
}
|
||||
} )
|
||||
}
|
||||
|
||||
return related;
|
||||
}
|
||||
|
||||
$: relatedTools = getRelatedTools( tool );
|
||||
|
||||
/**
|
||||
* Starts a tool's job.
|
||||
*
|
||||
* @param {Object} tool
|
||||
*/
|
||||
function handleStartTool( tool ) {
|
||||
tools.start( tool );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Start button click.
|
||||
*/
|
||||
function handleStart() {
|
||||
handleStartTool( tool );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel multi class="tools-panel">
|
||||
<PanelRow header>
|
||||
<img src={icon} type="image/svg+xml" alt={tool.title}>
|
||||
{#if showTotal}
|
||||
{#if initial}
|
||||
<h3>{@html tool.title}</h3>
|
||||
{:else if partialComplete}
|
||||
<h3>{@html tool.title_partial_complete}</h3>
|
||||
{:else}
|
||||
<h3>{@html tool.title_complete}</h3>
|
||||
{/if}
|
||||
{:else}
|
||||
<h3>{@html tool.title}</h3>
|
||||
{/if}
|
||||
<div class="buttons-right">
|
||||
{#if isRunning}
|
||||
<ToolRunningButtons {tool} {disabled}/>
|
||||
{:else}
|
||||
{#if complete}
|
||||
<!-- 🎉 -->
|
||||
{:else if disabled_bucket_access}
|
||||
<Button primary disabled={true} title={$strings.disabled_tool_bucket_access}>{@html partialComplete ? tool.button_partial_complete : tool.button}</Button>
|
||||
{:else if initial}
|
||||
<Button primary {disabled} title={disabled ? $strings.disabled_tool_button : ""} on:click={handleStart}>{@html tool.button}</Button>
|
||||
{:else if partialComplete}
|
||||
<Button primary {disabled} title={disabled ? $strings.disabled_tool_button : ""} on:click={handleStart}>{@html tool.button_partial_complete}</Button>
|
||||
{:else}
|
||||
<Button primary {disabled} title={disabled ? $strings.disabled_tool_button : ""} on:click={handleStart}>{@html tool.button}</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</PanelRow>
|
||||
{#if complete || partialComplete || isRunning}
|
||||
<PanelRow class="body flex-row show-progress">
|
||||
<div class="status">
|
||||
{#if isRunning}
|
||||
<h4>
|
||||
<strong>{getPercentComplete( tool, isRunning, showTotal )}%</strong> ({numToString( tool.queue.processed )}/{numToString( tool.queue.total )})
|
||||
{@html tool.status_description ? " " + tool.status_description : " " + tool.busy_description}
|
||||
</h4>
|
||||
{:else }
|
||||
<h4>{@html tool.progress_description}</h4>
|
||||
{/if}
|
||||
<slot name="status-right" {isRunning}/>
|
||||
</div>
|
||||
<ProgressBar
|
||||
{percentComplete}
|
||||
{starting}
|
||||
running={isRunning}
|
||||
paused={tool.is_paused}
|
||||
title={! isRunning && showTotal ? "(" + numToString(tool.total_processed) + "/" + numToString(tool.total_items) + ")" : ""}
|
||||
/>
|
||||
</PanelRow>
|
||||
{/if}
|
||||
{#if !complete && !partialComplete && !isRunning}
|
||||
<PanelRow class="body flex-row">
|
||||
<p class="desc">{@html tool.more_info}</p>
|
||||
</PanelRow>
|
||||
{#if !disabled && relatedTools.size > 0 }
|
||||
<PanelRow class="body flex-column" footer>
|
||||
{#each [...relatedTools] as [key, relatedTool] }
|
||||
<p>
|
||||
<a
|
||||
href={$urls.settings}
|
||||
on:click|preventDefault={() => handleStartTool(relatedTool)}
|
||||
title={relatedTool.more_info}
|
||||
>
|
||||
{relatedTool.title}
|
||||
</a>
|
||||
</p>
|
||||
{/each}
|
||||
</PanelRow>
|
||||
{/if}
|
||||
{/if}
|
||||
</Panel>
|
||||
34
ui/pro/ToolRunningButtons.svelte
Normal file
34
ui/pro/ToolRunningButtons.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import {tools} from "./stores";
|
||||
import {strings} from "../js/stores";
|
||||
import Button from "../components/Button.svelte";
|
||||
|
||||
export let tool = {};
|
||||
export let disabled = false;
|
||||
export let small = false;
|
||||
|
||||
/**
|
||||
* Handles a Pause or Resume button click.
|
||||
*/
|
||||
function handlePauseResume() {
|
||||
tools.pauseResume( tool );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Cancel button click.
|
||||
*/
|
||||
function handleCancel() {
|
||||
tools.cancel( tool );
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if tool}
|
||||
<Button outline {small} {disabled} class="pause" on:click={handlePauseResume}>
|
||||
{#if tool.is_paused}
|
||||
{$strings.resume_button}
|
||||
{:else}
|
||||
{$strings.pause_button}
|
||||
{/if}
|
||||
</Button>
|
||||
<Button outline {small} {disabled} on:click={handleCancel}>{$strings.cancel_button}</Button>
|
||||
{/if}
|
||||
94
ui/pro/ToolRunningStatus.svelte
Normal file
94
ui/pro/ToolRunningStatus.svelte
Normal file
@@ -0,0 +1,94 @@
|
||||
<script>
|
||||
import {push} from "svelte-spa-router";
|
||||
import {urls} from "../js/stores";
|
||||
import {running, tools, toolsLocked} from "./stores";
|
||||
import {numToShortString, numToString} from "../js/numToString";
|
||||
import ProgressBar from "../components/ProgressBar.svelte";
|
||||
import ToolRunningButtons from "./ToolRunningButtons.svelte";
|
||||
|
||||
/**
|
||||
* Returns the currently running tool's details.
|
||||
*
|
||||
* @param {Object} tools
|
||||
* @param {string} running
|
||||
*
|
||||
* @return {unknown}
|
||||
*/
|
||||
function runningTool( tools, running ) {
|
||||
return Object.values( tools ).find( ( tool ) => tool.id === running );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status description for tool.
|
||||
*
|
||||
* @param {Object} tool
|
||||
* @param {boolean} isRunning
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function toolStatus( tool, isRunning ) {
|
||||
if ( !isRunning ) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if ( tool.short_status_description ) {
|
||||
return tool.short_status_description;
|
||||
}
|
||||
|
||||
return tool.busy_description;
|
||||
}
|
||||
|
||||
$: isRunning = !!$running;
|
||||
$: tool = runningTool( $tools, $running );
|
||||
$: icon = tools.icon( tool, isRunning, true );
|
||||
|
||||
// Buttons should be disabled if another tool is running, current tool is in process of pausing or cancelling, or all tools locked.
|
||||
$: disabled = isRunning && (($running && $running !== tool.id) || (tool.is_processing && tool.is_paused) || tool.is_cancelled || $toolsLocked);
|
||||
|
||||
$: starting = !!(isRunning && tool.progress < 1 && !tool.is_paused);
|
||||
$: status = isRunning ? "(" + numToShortString( tool.queue.processed ) + "/" + numToShortString( tool.queue.total ) + ") " + toolStatus( tool, isRunning ) : "";
|
||||
$: title = isRunning ? tool.name + ": " + tool.progress + "% (" + numToString( tool.queue.processed ) + "/" + numToString( tool.queue.total ) + ")" : "";
|
||||
|
||||
/**
|
||||
* Returns the numeric percentage progress for the running job.
|
||||
*
|
||||
* @param {Object} tool
|
||||
* @param {boolean} isRunning
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
function getPercentComplete( tool, isRunning ) {
|
||||
if ( isRunning ) {
|
||||
return tool.progress;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$: percentComplete = getPercentComplete( tool, isRunning );
|
||||
</script>
|
||||
|
||||
{#if tool}
|
||||
<div class="nav-status-wrapper tool-running">
|
||||
<!-- TODO: Fix a11y. -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="nav-status" {title} on:click={() => push("/tools")}>
|
||||
<p class="status-text" {title}>
|
||||
<strong>{tool.progress}%</strong>
|
||||
<span> {@html status}</span>
|
||||
</p>
|
||||
<ProgressBar
|
||||
{percentComplete}
|
||||
{starting}
|
||||
running={isRunning}
|
||||
paused={tool.is_paused}
|
||||
{title}
|
||||
/>
|
||||
<div class="animation-running" {title}>
|
||||
<img src="{$urls.assets + 'img/icon/' + icon}" alt="{tool.status_description}"/>
|
||||
</div>
|
||||
</div>
|
||||
<ToolRunningButtons {tool} {disabled} small/>
|
||||
</div>
|
||||
{/if}
|
||||
29
ui/pro/ToolsPage.svelte
Normal file
29
ui/pro/ToolsPage.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import {setContext} from "svelte";
|
||||
import {strings} from "../js/stores";
|
||||
import {tools, toolsLocked} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import ToolPanel from "./ToolPanel.svelte";
|
||||
import NoTools from "./NoTools.svelte";
|
||||
|
||||
export let name = "tools";
|
||||
|
||||
// Let all child components know if tools are currently locked.
|
||||
// All panels etc respond to settingsLocked, so we fake it here as we're not in a settings context.
|
||||
setContext( "settingsLocked", toolsLocked );
|
||||
</script>
|
||||
|
||||
<Page {name} on:routeEvent>
|
||||
<Notifications tab={name} component={ToolNotification}/>
|
||||
<h2 class="page-title">{$strings.tools_title}</h2>
|
||||
|
||||
<div class="tools-page wrapper">
|
||||
{#each Object.values( $tools ).filter( ( tool ) => tool.render ) as tool (tool.id)}
|
||||
<ToolPanel {tool}/>
|
||||
{:else}
|
||||
<NoTools/>
|
||||
{/each}
|
||||
</div>
|
||||
</Page>
|
||||
62
ui/pro/UpdateObjectACLsPromptSubPage.svelte
Normal file
62
ui/pro/UpdateObjectACLsPromptSubPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext, hasContext} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
import {pop} from "svelte-spa-router";
|
||||
import {strings} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import SubPage from "../components/SubPage.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
|
||||
const tool = $tools.update_acls;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Parent page may want to be locked.
|
||||
let settingsLocked = writable( false );
|
||||
|
||||
if ( hasContext( "settingsLocked" ) ) {
|
||||
settingsLocked = getContext( "settingsLocked" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<SubPage name="update-acls" route="/storage/update-acls">
|
||||
<Panel
|
||||
heading={tool.title}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
skipText={$strings.no}
|
||||
nextText={$strings.yes}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</SubPage>
|
||||
62
ui/pro/UploaderPromptPage.svelte
Normal file
62
ui/pro/UploaderPromptPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {settingsLocked} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
|
||||
export let name = "uploader";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const tool = $tools.uploader;
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" component={ToolNotification}/>
|
||||
|
||||
<Panel
|
||||
heading={tool.name}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
nextText={tool.button}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</Page>
|
||||
615
ui/pro/pages.js
Normal file
615
ui/pro/pages.js
Normal file
@@ -0,0 +1,615 @@
|
||||
import {get} from "svelte/store";
|
||||
import {location} from "svelte-spa-router";
|
||||
import {
|
||||
bapa,
|
||||
ooe,
|
||||
counts,
|
||||
current_settings,
|
||||
storage_provider,
|
||||
strings
|
||||
} from "../js/stores";
|
||||
import {pages} from "../js/routes";
|
||||
import {licence} from "./stores";
|
||||
import AssetsPage from "./AssetsPage.svelte";
|
||||
import ToolsPage from "./ToolsPage.svelte";
|
||||
import LicencePage from "./LicencePage.svelte";
|
||||
import SupportPage from "./SupportPage.svelte";
|
||||
import UpdateObjectACLsPromptSubPage
|
||||
from "./UpdateObjectACLsPromptSubPage.svelte";
|
||||
import CopyBucketsPromptSubPage from "./CopyBucketsPromptSubPage.svelte";
|
||||
import MoveObjectsPromptPage from "./MoveObjectsPromptPage.svelte";
|
||||
import MovePublicObjectsPromptPage from "./MovePublicObjectsPromptPage.svelte";
|
||||
import MovePrivateObjectsPromptPage
|
||||
from "./MovePrivateObjectsPromptPage.svelte";
|
||||
import RemoveLocalFilesPromptPage from "./RemoveLocalFilesPromptPage.svelte";
|
||||
import UploaderPromptPage from "./UploaderPromptPage.svelte";
|
||||
import DownloaderPromptPage from "./DownloaderPromptPage.svelte";
|
||||
|
||||
export function addPages( enabledTools ) {
|
||||
pages.add(
|
||||
{
|
||||
position: 10,
|
||||
name: "assets",
|
||||
title: () => get( strings ).assets_tab_title,
|
||||
nav: true,
|
||||
route: "/assets",
|
||||
component: AssetsPage
|
||||
}
|
||||
);
|
||||
pages.add(
|
||||
{
|
||||
position: 20,
|
||||
name: "tools",
|
||||
title: () => get( strings ).tools_tab_title,
|
||||
nav: true,
|
||||
route: "/tools",
|
||||
component: ToolsPage
|
||||
}
|
||||
);
|
||||
pages.add(
|
||||
{
|
||||
position: 90,
|
||||
name: "licence",
|
||||
title: () => get( strings ).licence_tab_title,
|
||||
nav: true,
|
||||
route: "/license",
|
||||
component: LicencePage
|
||||
}
|
||||
);
|
||||
pages.add(
|
||||
{
|
||||
position: 100,
|
||||
name: "support",
|
||||
title: () => get( strings ).support_tab_title,
|
||||
nav: true,
|
||||
route: "/support",
|
||||
component: SupportPage
|
||||
}
|
||||
);
|
||||
|
||||
// Update ACLs tool prompt.
|
||||
if ( enabledTools.hasOwnProperty( "update_acls" ) ) {
|
||||
const updateACLs = {
|
||||
position: 240,
|
||||
name: "update-acls",
|
||||
title: () => enabledTools.update_acls.name,
|
||||
subNav: true,
|
||||
route: "/storage/update-acls",
|
||||
component: UpdateObjectACLsPromptSubPage,
|
||||
enabled: () => {
|
||||
// Nothing to update?
|
||||
if (
|
||||
!get( counts ).hasOwnProperty( "offloaded" ) ||
|
||||
get( counts ).offloaded < 1 ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Current Storage Provider never allows ACLs to be disabled.
|
||||
if ( get( storage_provider ).requires_acls ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If either Block All Public Access or Object Ownership turned on,
|
||||
// we should not update ACLs.
|
||||
if ( get( bapa ) || get( ooe ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update ACLs if BAPA just turned off.
|
||||
if (
|
||||
get( storage_provider ).block_public_access_supported &&
|
||||
get( current_settings ).hasOwnProperty( "block-public-access" ) &&
|
||||
updateACLs.blockPublicAccess !== get( current_settings )[ "block-public-access" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Update ACLs if OOE just turned off.
|
||||
if (
|
||||
get( storage_provider ).object_ownership_supported &&
|
||||
get( current_settings ).hasOwnProperty( "object-ownership-enforced" ) &&
|
||||
updateACLs.objectOwnershipEnforced !== get( current_settings )[ "object-ownership-enforced" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isNextRoute: ( data ) => {
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!updateACLs.enabled()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If currently in /storage/security route, then update-acls is next.
|
||||
return get( location ) === "/storage/security";
|
||||
},
|
||||
blockPublicAccess: get( current_settings ).hasOwnProperty( "bucket" ) && get( current_settings ).hasOwnProperty( "block-public-access" ) ? get( current_settings )[ "block-public-access" ] : false,
|
||||
objectOwnershipEnforced: get( current_settings ).hasOwnProperty( "bucket" ) && get( current_settings ).hasOwnProperty( "object-ownership-enforced" ) ? get( current_settings )[ "object-ownership-enforced" ] : false,
|
||||
setInitialProperties: ( data ) => {
|
||||
if ( data.hasOwnProperty( "settings" ) && data.settings.hasOwnProperty( "bucket" ) ) {
|
||||
if ( data.settings.hasOwnProperty( "block-public-access" ) ) {
|
||||
updateACLs.blockPublicAccess = data.settings[ "block-public-access" ];
|
||||
} else {
|
||||
updateACLs.blockPublicAccess = false;
|
||||
}
|
||||
|
||||
if ( data.settings.hasOwnProperty( "object-ownership-enforced" ) ) {
|
||||
updateACLs.objectOwnershipEnforced = data.settings[ "object-ownership-enforced" ];
|
||||
} else {
|
||||
updateACLs.objectOwnershipEnforced = false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
events: {
|
||||
"next": ( data ) => updateACLs.isNextRoute( data ),
|
||||
"bucket-security": ( data ) => updateACLs.isNextRoute( data ),
|
||||
"settings.save": ( data ) => updateACLs.setInitialProperties( data ),
|
||||
"page.initial.settings": ( data ) => updateACLs.setInitialProperties( data )
|
||||
}
|
||||
};
|
||||
pages.add( updateACLs );
|
||||
}
|
||||
|
||||
// Copy Files tool prompt.
|
||||
if ( enabledTools.hasOwnProperty( "copy_buckets" ) ) {
|
||||
const copyBuckets = {
|
||||
position: 250,
|
||||
name: "copy-buckets",
|
||||
title: () => enabledTools.copy_buckets.name,
|
||||
subNav: true,
|
||||
route: "/storage/copy-buckets",
|
||||
component: CopyBucketsPromptSubPage,
|
||||
enabled: () => {
|
||||
return get( counts ).offloaded > 0 && get( current_settings ).hasOwnProperty( "bucket" ) && copyBuckets.bucket !== get( current_settings ).bucket;
|
||||
},
|
||||
isNextRoute: ( data ) => {
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!copyBuckets.enabled()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If currently in any of the below routes, then copy-buckets is next if not gazumped.
|
||||
return get( location ) === "/storage/bucket" || get( location ) === "/storage/security" || get( location ) === "/storage/update-acls";
|
||||
},
|
||||
bucket: get( current_settings ).hasOwnProperty( "bucket" ) ? get( current_settings ).bucket : "",
|
||||
setInitialBucket: ( data ) => {
|
||||
if ( data.hasOwnProperty( "settings" ) && data.settings.hasOwnProperty( "bucket" ) ) {
|
||||
copyBuckets.bucket = data.settings.bucket;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
events: {
|
||||
"next": ( data ) => copyBuckets.isNextRoute( data ),
|
||||
"settings.save": ( data ) => copyBuckets.isNextRoute( data ),
|
||||
"bucket-security": ( data ) => copyBuckets.isNextRoute( data ),
|
||||
"page.initial.settings": ( data ) => copyBuckets.setInitialBucket( data )
|
||||
}
|
||||
};
|
||||
pages.add( copyBuckets );
|
||||
}
|
||||
|
||||
// Move Public/Private Objects tool prompt.
|
||||
if (
|
||||
enabledTools.hasOwnProperty( "move_objects" ) &&
|
||||
enabledTools.hasOwnProperty( "move_public_objects" ) &&
|
||||
enabledTools.hasOwnProperty( "move_private_objects" )
|
||||
) {
|
||||
const moveObjects = {
|
||||
position: 400,
|
||||
name: "move-objects",
|
||||
title: () => enabledTools.move_objects.name,
|
||||
route: "/prompt/move-objects",
|
||||
component: MoveObjectsPromptPage,
|
||||
publicPathChanged: ( data ) => {
|
||||
if ( data.hasOwnProperty( "changed_settings" ) ) {
|
||||
// Year/Month disabled - never show prompt.
|
||||
if (
|
||||
data.changed_settings.includes( "use-yearmonth-folders" ) &&
|
||||
get( current_settings ).hasOwnProperty( "use-yearmonth-folders" ) &&
|
||||
!get( current_settings )[ "use-yearmonth-folders" ]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Object Versioning disabled - never show prompt.
|
||||
if (
|
||||
data.changed_settings.includes( "object-versioning" ) &&
|
||||
get( current_settings ).hasOwnProperty( "object-versioning" ) &&
|
||||
!get( current_settings )[ "object-versioning" ]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Path enabled/disabled.
|
||||
if (
|
||||
data.changed_settings.includes( "enable-object-prefix" ) &&
|
||||
get( current_settings ).hasOwnProperty( "enable-object-prefix" )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Path changed while enabled.
|
||||
if (
|
||||
data.changed_settings.includes( "object-prefix" ) &&
|
||||
get( current_settings ).hasOwnProperty( "enable-object-prefix" ) &&
|
||||
get( current_settings )[ "enable-object-prefix" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Year/Month enabled.
|
||||
if (
|
||||
data.changed_settings.includes( "use-yearmonth-folders" ) &&
|
||||
get( current_settings ).hasOwnProperty( "use-yearmonth-folders" ) &&
|
||||
get( current_settings )[ "use-yearmonth-folders" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Object Versioning enabled.
|
||||
if (
|
||||
data.changed_settings.includes( "object-versioning" ) &&
|
||||
get( current_settings ).hasOwnProperty( "object-versioning" ) &&
|
||||
get( current_settings )[ "object-versioning" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
privatePathChanged: ( data ) => {
|
||||
if ( data.hasOwnProperty( "changed_settings" ) ) {
|
||||
// Signed URLs enabled/disabled.
|
||||
if (
|
||||
data.changed_settings.includes( "enable-signed-urls" ) &&
|
||||
get( current_settings ).hasOwnProperty( "enable-signed-urls" )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Signed URLs prefix changed while enabled.
|
||||
if (
|
||||
data.changed_settings.includes( "signed-urls-object-prefix" ) &&
|
||||
get( current_settings ).hasOwnProperty( "enable-signed-urls" ) &&
|
||||
get( current_settings )[ "enable-signed-urls" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isNextRoute: ( data ) => {
|
||||
// Anything to work with?
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ||
|
||||
!get( counts ).hasOwnProperty( "offloaded" ) ||
|
||||
get( counts ).offloaded < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moveObjects.publicPathChanged( data ) && moveObjects.privatePathChanged( data );
|
||||
},
|
||||
events: {
|
||||
"settings.save": ( data ) => moveObjects.isNextRoute( data )
|
||||
}
|
||||
};
|
||||
pages.add( moveObjects );
|
||||
|
||||
const movePublicObjects = {
|
||||
position: 410,
|
||||
name: "move-public-objects",
|
||||
title: () => enabledTools.move_public_objects.name,
|
||||
route: "/prompt/move-public-objects",
|
||||
component: MovePublicObjectsPromptPage,
|
||||
isNextRoute: ( data ) => {
|
||||
// Anything to work with?
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ||
|
||||
!get( counts ).hasOwnProperty( "offloaded" ) ||
|
||||
get( counts ).offloaded < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moveObjects.publicPathChanged( data );
|
||||
},
|
||||
events: {
|
||||
"settings.save": ( data ) => movePublicObjects.isNextRoute( data )
|
||||
}
|
||||
};
|
||||
pages.add( movePublicObjects );
|
||||
|
||||
const movePrivateObjects = {
|
||||
position: 420,
|
||||
name: "move-private-objects",
|
||||
title: () => enabledTools.move_private_objects.name,
|
||||
route: "/prompt/move-private-objects",
|
||||
component: MovePrivateObjectsPromptPage,
|
||||
isNextRoute: ( data ) => {
|
||||
// Anything to work with?
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ||
|
||||
!get( counts ).hasOwnProperty( "offloaded" ) ||
|
||||
get( counts ).offloaded < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moveObjects.privatePathChanged( data );
|
||||
},
|
||||
events: {
|
||||
"settings.save": ( data ) => movePrivateObjects.isNextRoute( data )
|
||||
}
|
||||
};
|
||||
pages.add( movePrivateObjects );
|
||||
}
|
||||
|
||||
// Remove Local Files tool prompt.
|
||||
if ( enabledTools.hasOwnProperty( "remove_local_files" ) ) {
|
||||
const removeLocalFiles = {
|
||||
position: 430,
|
||||
name: "remove-local-files",
|
||||
title: () => enabledTools.remove_local_files.name,
|
||||
route: "/prompt/remove-local-files",
|
||||
component: RemoveLocalFilesPromptPage,
|
||||
onPreviousPage: () => {
|
||||
const previousPages = pages.withPrefix( "/prompt/" ).filter( ( page ) => page.position < removeLocalFiles.position );
|
||||
|
||||
for ( const previousPage of previousPages ) {
|
||||
if ( get( location ) === previousPage.route ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
removeLocalFile: get( current_settings ).hasOwnProperty( "remove-local-file" ) ? get( current_settings )[ "remove-local-file" ] : false,
|
||||
setInitialRemoveLocalFile: ( data ) => {
|
||||
if (
|
||||
get( location ) !== removeLocalFiles.route &&
|
||||
!removeLocalFiles.onPreviousPage() &&
|
||||
data.hasOwnProperty( "settings" ) &&
|
||||
data.settings.hasOwnProperty( "remove-local-file" )
|
||||
) {
|
||||
removeLocalFiles.removeLocalFile = data.settings[ "remove-local-file" ];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isNextRoute: ( data ) => {
|
||||
// Anything to work with?
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ||
|
||||
!get( counts ).hasOwnProperty( "offloaded" ) ||
|
||||
get( counts ).offloaded < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( data.hasOwnProperty( "changed_settings" ) ) {
|
||||
// Remove Local Files turned on.
|
||||
if (
|
||||
data.changed_settings.includes( "remove-local-file" ) &&
|
||||
get( current_settings ).hasOwnProperty( "remove-local-file" ) &&
|
||||
removeLocalFiles.removeLocalFile !== get( current_settings )[ "remove-local-file" ] &&
|
||||
get( current_settings )[ "remove-local-file" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Setting changed and event from previous prompt page.
|
||||
if (
|
||||
removeLocalFiles.onPreviousPage() &&
|
||||
get( current_settings ).hasOwnProperty( "remove-local-file" ) &&
|
||||
removeLocalFiles.removeLocalFile !== get( current_settings )[ "remove-local-file" ] &&
|
||||
get( current_settings )[ "remove-local-file" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We're not interested in showing prompt, just ensure local state is up to date.
|
||||
// NOTE: This handles syncing the local state when moving on from this prompt too.
|
||||
if ( get( current_settings ).hasOwnProperty( "remove-local-file" ) ) {
|
||||
removeLocalFiles.removeLocalFile = get( current_settings )[ "remove-local-file" ];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
events: {
|
||||
"next": ( data ) => removeLocalFiles.isNextRoute( data ),
|
||||
"settings.save": ( data ) => removeLocalFiles.isNextRoute( data ),
|
||||
"page.initial.settings": ( data ) => removeLocalFiles.setInitialRemoveLocalFile( data )
|
||||
}
|
||||
};
|
||||
pages.add( removeLocalFiles );
|
||||
}
|
||||
|
||||
// Uploader tool prompt.
|
||||
if ( enabledTools.hasOwnProperty( "uploader" ) ) {
|
||||
const uploader = {
|
||||
position: 440,
|
||||
name: "uploader",
|
||||
title: () => enabledTools.uploader.name,
|
||||
route: "/prompt/uploader",
|
||||
component: UploaderPromptPage,
|
||||
onPreviousPage: () => {
|
||||
const previousPages = pages.withPrefix( "/prompt/" ).filter( ( page ) => page.position < uploader.position );
|
||||
|
||||
for ( const previousPage of previousPages ) {
|
||||
if ( get( location ) === previousPage.route ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
copyToProvider: get( current_settings ).hasOwnProperty( "copy-to-s3" ) ? get( current_settings )[ "copy-to-s3" ] : false,
|
||||
setInitialCopyToProvider: ( data ) => {
|
||||
if (
|
||||
get( location ) !== uploader.route &&
|
||||
!uploader.onPreviousPage() &&
|
||||
data.hasOwnProperty( "settings" ) &&
|
||||
data.settings.hasOwnProperty( "copy-to-s3" )
|
||||
) {
|
||||
uploader.copyToProvider = data.settings[ "copy-to-s3" ];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isNextRoute: ( data ) => {
|
||||
// Anything to work with?
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ||
|
||||
!get( counts ).hasOwnProperty( "not_offloaded" ) ||
|
||||
get( counts ).not_offloaded < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( data.hasOwnProperty( "changed_settings" ) ) {
|
||||
// Copy to Provider turned on.
|
||||
if (
|
||||
data.changed_settings.includes( "copy-to-s3" ) &&
|
||||
get( current_settings ).hasOwnProperty( "copy-to-s3" ) &&
|
||||
uploader.copyToProvider !== get( current_settings )[ "copy-to-s3" ] &&
|
||||
get( current_settings )[ "copy-to-s3" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Setting changed and event from previous prompt page.
|
||||
if (
|
||||
uploader.onPreviousPage() &&
|
||||
get( current_settings ).hasOwnProperty( "copy-to-s3" ) &&
|
||||
uploader.copyToProvider !== get( current_settings )[ "copy-to-s3" ] &&
|
||||
get( current_settings )[ "copy-to-s3" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We're not interested in showing prompt, just ensure local state is up to date.
|
||||
// NOTE: This handles syncing the local state when moving on from this prompt too.
|
||||
if ( get( current_settings ).hasOwnProperty( "copy-to-s3" ) ) {
|
||||
uploader.copyToProvider = get( current_settings )[ "copy-to-s3" ];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
events: {
|
||||
"next": ( data ) => uploader.isNextRoute( data ),
|
||||
"settings.save": ( data ) => uploader.isNextRoute( data ),
|
||||
"page.initial.settings": ( data ) => uploader.setInitialCopyToProvider( data )
|
||||
}
|
||||
};
|
||||
pages.add( uploader );
|
||||
}
|
||||
|
||||
// Downloader tool prompt when Remove Local Files turned off.
|
||||
if ( enabledTools.hasOwnProperty( "downloader" ) ) {
|
||||
const downloader = {
|
||||
position: 450,
|
||||
name: "downloader",
|
||||
title: () => enabledTools.downloader.name,
|
||||
route: "/prompt/downloader",
|
||||
component: DownloaderPromptPage,
|
||||
onPreviousPage: () => {
|
||||
const previousPages = pages.withPrefix( "/prompt/" ).filter( ( page ) => page.position < downloader.position );
|
||||
|
||||
for ( const previousPage of previousPages ) {
|
||||
if ( get( location ) === previousPage.route ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
removeLocalFile: get( current_settings ).hasOwnProperty( "remove-local-file" ) ? get( current_settings )[ "remove-local-file" ] : false,
|
||||
setInitialRemoveLocalFile: ( data ) => {
|
||||
if (
|
||||
get( location ) !== downloader.route &&
|
||||
!downloader.onPreviousPage() &&
|
||||
data.hasOwnProperty( "settings" ) &&
|
||||
data.settings.hasOwnProperty( "remove-local-file" )
|
||||
) {
|
||||
downloader.removeLocalFile = data.settings[ "remove-local-file" ];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isNextRoute: ( data ) => {
|
||||
// Anything to work with?
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ||
|
||||
!get( counts ).hasOwnProperty( "offloaded" ) ||
|
||||
get( counts ).offloaded < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( data.hasOwnProperty( "changed_settings" ) ) {
|
||||
// Remove Local Files turned off.
|
||||
if (
|
||||
data.changed_settings.includes( "remove-local-file" ) &&
|
||||
get( current_settings ).hasOwnProperty( "remove-local-file" ) &&
|
||||
downloader.removeLocalFile !== get( current_settings )[ "remove-local-file" ] &&
|
||||
!get( current_settings )[ "remove-local-file" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Setting changed and event from previous prompt page.
|
||||
if (
|
||||
downloader.onPreviousPage() &&
|
||||
get( current_settings ).hasOwnProperty( "remove-local-file" ) &&
|
||||
downloader.removeLocalFile !== get( current_settings )[ "remove-local-file" ] &&
|
||||
!get( current_settings )[ "remove-local-file" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We're not interested in showing prompt, just ensure local state is up to date.
|
||||
// NOTE: This handles syncing the local state when moving on from this prompt too.
|
||||
if ( get( current_settings ).hasOwnProperty( "remove-local-file" ) ) {
|
||||
downloader.removeLocalFile = get( current_settings )[ "remove-local-file" ];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
events: {
|
||||
"next": ( data ) => downloader.isNextRoute( data ),
|
||||
"settings.save": ( data ) => downloader.isNextRoute( data ),
|
||||
"page.initial.settings": ( data ) => downloader.setInitialRemoveLocalFile( data )
|
||||
}
|
||||
};
|
||||
pages.add( downloader );
|
||||
}
|
||||
}
|
||||
251
ui/pro/stores.js
Normal file
251
ui/pro/stores.js
Normal file
@@ -0,0 +1,251 @@
|
||||
import {derived, get, writable} from "svelte/store";
|
||||
import {api, config, state} from "../js/stores";
|
||||
import {objectsDiffer} from "../js/objectsDiffer";
|
||||
|
||||
// Convenience readable store of licence, derived from config.
|
||||
// We currently have one licence applied to a plugin install.
|
||||
export const licence = derived( config, $config => $config.hasOwnProperty( "licences" ) ? $config.licences.at( 0 ) : [] );
|
||||
|
||||
// Convenience readable store of offload remaining with count message, derived from config.
|
||||
export const offloadRemainingWithCount = derived( config, $config => $config.offload_remaining_with_count );
|
||||
|
||||
// Convenience readable store of documentation, derived from config.
|
||||
export const documentation = derived( config, $config => $config.documentation );
|
||||
|
||||
/*
|
||||
* Tools.
|
||||
*/
|
||||
|
||||
// Whether tools are locked due to background activity such as upgrade.
|
||||
export const toolsLocked = writable( false );
|
||||
|
||||
// Keeps track of the currently running background tool.
|
||||
export const running = writable( "" );
|
||||
|
||||
const toolIcons = {
|
||||
"add-metadata": "offload",
|
||||
"reverse-add-metadata": "analyzerepair",
|
||||
"verify-add-metadata": "analyzerepair",
|
||||
"copy-buckets": "move",
|
||||
"download-and-remover": "remove",
|
||||
"downloader": "download",
|
||||
"elementor-analyze-and-repair": "analyzerepair",
|
||||
"move-objects": "move",
|
||||
"move-private-objects": "move",
|
||||
"move-public-objects": "move",
|
||||
"remove-local-files": "clean",
|
||||
"update-acls": "analyzerepair",
|
||||
"uploader": "offload",
|
||||
"woocommerce-product-urls": "analyzerepair",
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates store of tools info and API access methods.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function createTools() {
|
||||
const { subscribe, set, update } = writable( {} );
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
async action( tool, action ) {
|
||||
state.pausePeriodicFetch();
|
||||
|
||||
// Set the status text to the default busy description
|
||||
// until the API returns a calculated status description.
|
||||
tool.status_description = tool.busy_description;
|
||||
tool.short_status_description = tool.busy_description;
|
||||
|
||||
// Ensure all subscribers know the tool status is changing.
|
||||
update( _tools => {
|
||||
_tools[ tool.id ] = tool;
|
||||
|
||||
return _tools;
|
||||
} );
|
||||
|
||||
let result = {};
|
||||
const json = await api.put( "tools", {
|
||||
id: tool.id,
|
||||
action: action
|
||||
} );
|
||||
|
||||
if ( json.hasOwnProperty( "ok" ) ) {
|
||||
result = json;
|
||||
}
|
||||
|
||||
await state.resumePeriodicFetch();
|
||||
return result;
|
||||
},
|
||||
async start( tool ) {
|
||||
// Ensure all subscribers know that a tool is running.
|
||||
running.update( _running => tool.id );
|
||||
tool.is_queued = true;
|
||||
|
||||
return await this.action( tool, "start" );
|
||||
},
|
||||
async cancel( tool ) {
|
||||
tool.is_cancelled = true;
|
||||
|
||||
return await this.action( tool, "cancel" );
|
||||
},
|
||||
async pauseResume( tool ) {
|
||||
tool.is_paused = !tool.is_paused;
|
||||
|
||||
return await this.action( tool, "pause_resume" );
|
||||
},
|
||||
updateTools( json ) {
|
||||
if ( json.hasOwnProperty( "tools" ) ) {
|
||||
// Update our understanding of what the server's tools status is.
|
||||
update( _tools => {
|
||||
return { ...json.tools };
|
||||
} );
|
||||
|
||||
// Update our understanding of the currently running tool.
|
||||
const runningTool = Object.values( json.tools ).find( ( tool ) => tool.is_processing || tool.is_queued || tool.is_paused || tool.is_cancelled );
|
||||
|
||||
if ( runningTool ) {
|
||||
running.update( _running => runningTool.id );
|
||||
} else {
|
||||
running.update( _running => "" );
|
||||
}
|
||||
}
|
||||
},
|
||||
icon( tool, isRunning, animated ) {
|
||||
let icon = "tool-generic";
|
||||
let type = "default";
|
||||
|
||||
if ( isRunning ) {
|
||||
if ( tool.is_paused ) {
|
||||
type = "paused";
|
||||
} else if ( animated ) {
|
||||
type = "running-animated";
|
||||
} else {
|
||||
type = "active";
|
||||
}
|
||||
}
|
||||
|
||||
if ( tool && tool.hasOwnProperty( "slug" ) && toolIcons.hasOwnProperty( tool.slug ) ) {
|
||||
icon = "tool-" + toolIcons[ tool.slug ];
|
||||
}
|
||||
|
||||
if ( ["active", "default", "paused", "running-animated"].includes( type ) ) {
|
||||
icon = icon + "-" + type + ".svg";
|
||||
} else {
|
||||
icon = icon + "-default.svg";
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const tools = createTools();
|
||||
|
||||
/*
|
||||
* Assets.
|
||||
*/
|
||||
|
||||
// Does the app need a page refresh to resolve conflicts?
|
||||
export const assetsNeedsRefresh = writable( false );
|
||||
|
||||
// Whether assets settings are locked due to background activity such as upgrade.
|
||||
export const assetsSettingsLocked = writable( false );
|
||||
|
||||
// Convenience readable store of server's assets settings, derived from config.
|
||||
export const currentAssetsSettings = derived( config, $config => $config.assets_settings );
|
||||
|
||||
// Convenience readable store of defined assets settings keys, derived from config.
|
||||
export const assetsDefinedSettings = derived( config, $config => $config.assets_defined_settings );
|
||||
|
||||
// Convenience readable store of assets domain check info, derived from config.
|
||||
export const assetsDomainCheck = derived( config, $config => $config.assets_domain_check );
|
||||
|
||||
// Convenience readable store indicating whether Assets functionality may be used.
|
||||
export const enableAssets = derived( [licence, config], ( [$licence, $config] ) => {
|
||||
if (
|
||||
$licence.hasOwnProperty( "is_set" ) &&
|
||||
$licence.is_set &&
|
||||
$licence.hasOwnProperty( "is_valid" ) &&
|
||||
$licence.is_valid &&
|
||||
$config.hasOwnProperty( "assets_settings" )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} );
|
||||
|
||||
/**
|
||||
* Creates store of assets settings.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function createAssetsSettings() {
|
||||
const { subscribe, set, update } = writable( [] );
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
async save() {
|
||||
const json = await api.put( "assets-settings", get( this ) );
|
||||
|
||||
if ( json.hasOwnProperty( "saved" ) && true === json.saved ) {
|
||||
// Sync settings with what the server has.
|
||||
this.updateSettings( json );
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
reset() {
|
||||
set( { ...get( currentAssetsSettings ) } );
|
||||
},
|
||||
async fetch() {
|
||||
const json = await api.get( "assets-settings", {} );
|
||||
this.updateSettings( json );
|
||||
},
|
||||
updateSettings( json ) {
|
||||
if (
|
||||
json.hasOwnProperty( "assets_defined_settings" ) &&
|
||||
json.hasOwnProperty( "assets_settings" )
|
||||
) {
|
||||
const dirty = get( assetsSettingsChanged );
|
||||
const previousSettings = { ...get( currentAssetsSettings ) }; // cloned
|
||||
|
||||
// Update our understanding of what the server's settings are.
|
||||
config.update( _config => {
|
||||
return {
|
||||
..._config,
|
||||
assets_defined_settings: json.assets_defined_settings,
|
||||
assets_settings: json.assets_settings,
|
||||
};
|
||||
} );
|
||||
|
||||
// No need to check for changes from state if we've just saved these settings.
|
||||
if ( json.hasOwnProperty( "saved" ) && true === json.saved ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the settings weren't changed before, they shouldn't be now.
|
||||
if ( !dirty && get( assetsSettingsChanged ) ) {
|
||||
assetsSettings.reset();
|
||||
}
|
||||
|
||||
// If settings are in middle of being changed when changes come in
|
||||
// from server, reset to server version.
|
||||
if ( dirty && objectsDiffer( [previousSettings, get( currentAssetsSettings )] ) ) {
|
||||
assetsNeedsRefresh.update( _needsRefresh => true );
|
||||
assetsSettings.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const assetsSettings = createAssetsSettings();
|
||||
|
||||
// Have the assets settings been changed from current server side settings?
|
||||
export const assetsSettingsChanged = derived( [assetsSettings, currentAssetsSettings], objectsDiffer );
|
||||
67
ui/pro/toolSettingsNotifications.js
Normal file
67
ui/pro/toolSettingsNotifications.js
Normal file
@@ -0,0 +1,67 @@
|
||||
export const toolSettingsNotifications = {
|
||||
/**
|
||||
* Process local and server settings to return a new Map of inline notifications.
|
||||
*
|
||||
* @param {Map} notifications
|
||||
* @param {Object} settings
|
||||
* @param {Object} current_settings
|
||||
* @param {Object} strings
|
||||
* @param {Object} counts
|
||||
* @param {Object} licence
|
||||
*
|
||||
* @return {Map<string, Map<string, Object>>} keyed by setting name, containing map of notification objects keyed by id.
|
||||
*/
|
||||
process: ( notifications, settings, current_settings, strings, counts, licence ) => {
|
||||
// use-yearmonth-folders
|
||||
let entries = notifications.has( "use-yearmonth-folders" ) ? notifications.get( "use-yearmonth-folders" ) : new Map();
|
||||
if (
|
||||
current_settings.hasOwnProperty( "use-yearmonth-folders" ) &&
|
||||
current_settings[ "use-yearmonth-folders" ] &&
|
||||
settings.hasOwnProperty( "use-yearmonth-folders" ) &&
|
||||
!settings[ "use-yearmonth-folders" ] &&
|
||||
counts.hasOwnProperty( "offloaded" ) &&
|
||||
counts.offloaded > 0 &&
|
||||
licence.hasOwnProperty( "is_valid" ) &&
|
||||
licence.is_valid
|
||||
) {
|
||||
if ( !entries.has( "no-move-objects-year-month-notice" ) ) {
|
||||
entries.set( "no-move-objects-year-month-notice", {
|
||||
inline: true,
|
||||
type: "warning",
|
||||
message: strings.no_move_objects_year_month_notice
|
||||
} );
|
||||
}
|
||||
} else if ( entries.has( "no-move-objects-year-month-notice" ) ) {
|
||||
entries.delete( "no-move-objects-year-month-notice" );
|
||||
}
|
||||
|
||||
notifications.set( "use-yearmonth-folders", entries );
|
||||
|
||||
// object-versioning
|
||||
entries = notifications.has( "object-versioning" ) ? notifications.get( "object-versioning" ) : new Map();
|
||||
if (
|
||||
current_settings.hasOwnProperty( "object-versioning" ) &&
|
||||
current_settings[ "object-versioning" ] &&
|
||||
settings.hasOwnProperty( "object-versioning" ) &&
|
||||
!settings[ "object-versioning" ] &&
|
||||
counts.hasOwnProperty( "offloaded" ) &&
|
||||
counts.offloaded > 0 &&
|
||||
licence.hasOwnProperty( "is_valid" ) &&
|
||||
licence.is_valid
|
||||
) {
|
||||
if ( !entries.has( "no-move-objects-object-versioning-notice" ) ) {
|
||||
entries.set( "no-move-objects-object-versioning-notice", {
|
||||
inline: true,
|
||||
type: "warning",
|
||||
message: strings.no_move_objects_object_versioning_notice
|
||||
} );
|
||||
}
|
||||
} else if ( entries.has( "no-move-objects-object-versioning-notice" ) ) {
|
||||
entries.delete( "no-move-objects-object-versioning-notice" );
|
||||
}
|
||||
|
||||
notifications.set( "object-versioning", entries );
|
||||
|
||||
return notifications;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user