Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ OWA 2.x is supported in beta: set OWA Server Version to 2.x. The plugin then aut
* Multisite network mode (2.x): Network Admin > OWA takes the OWA endpoint and one application password for every site.
* Each site then chooses its Website ID from the Profiles that share its domain.
* Or the network configures each site's Profile in a mapping table, where each site starts on the Profile of exactly its domain. Opening the network settings page fetches the Profiles from OWA as it reads the network's sites, and marks new sites and new Profiles until the next save.
* In that mode each site also has a Tracking switch in the table; the network decides, and the site's own Enable OWA is hidden.
* New setting in both versions: Don't Track Logged-in Users.
* The settings pages are rebuilt with WordPress components (React). They save through the plugin's REST routes; keys and passwords are never sent back to the browser, and an invalid entry is reported under its field without saving anything.

Expand Down
27 changes: 27 additions & 0 deletions js/lib/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,34 @@ export function visibleFields( fields, version, section ) {
return fields.filter( ( field ) => isVisible( field, version ) && ( ! section || field.section === section ) );
}

/** A field's title and description for the chosen version: 2.x has its own wording for some. */
export function fieldText( field, version ) {
const v2 = version === '2' ? field.v2 || {} : {};
return { title: v2.title || field.title, description: v2.description || field.description };
}

/**
* What is left to set up, as the server found it in the saved settings,
* worded for the version chosen on the page; none while tracking is off.
*/
export function setupNotices( payload, values ) {
if ( ! Number( values.enable ) ) {
return [];
}
return ( payload.notices || {} )[ values.owaVersion === '2' ? '2' : '1' ] || [];
}

/** The tabs that have something to show. */
export function visibleSections( sections, fields, version ) {
return sections.filter( ( section ) => visibleFields( fields, version, section.name ).length > 0 );
}

/**
* The link to the Website ID's tracking tag screen in OWA shows only for what
* is saved: the server builds it from the saved settings, so it is hidden
* while the version, the endpoint or the Website ID differs from them.
*/
export function showTagSettingsLink( payload, values ) {
return Boolean( payload.tagSettingsUrl ) &&
[ 'owaVersion', 'owaEndpoint', 'siteId' ].every( ( key ) => values[ key ] === payload.values[ key ] );
}
48 changes: 39 additions & 9 deletions js/lib/mapping.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
* The network settings page's mapping table: each site's Profile.
*/

export const CHANGED = 'Changed: saved when you save';

/** A Profile's option text; one that appeared in OWA since the last save says so. */
export function profileLabel( profile ) {
return profile.new ? `${ profile.label } — new` : profile.label;
Expand All @@ -17,13 +15,34 @@ export function profilesFor( profiles, baseDomain ) {
} );
}

/**
* The status of a site's row for the value now selected: the server's status
* while the row is as it was loaded, "changed" once it is not.
*/
export function rowStatus( blog, value ) {
const loaded = blog.saved === null ? blog.selected : blog.saved;
return value === loaded ? blog.status : CHANGED;
/** A saved row's Profile, as its cell shows it until Edit is clicked. */
export function savedLabel( profiles, id ) {
if ( ! id ) {
return 'No Observation Profile';
}
const profile = profiles.find( ( p ) => p.id === id );
return profile ? profile.label : `${ id } (not in the list from OWA)`;
}

/** A row has a saved Profile: it shows it as text with Edit, and a Tracking switch. */
export function hasSavedProfile( blog ) {
return Boolean( blog.saved );
}

/** A row shows its dropdown until a Profile is saved for it, or after Edit. */
export function isEditing( blog, editing ) {
return ! hasSavedProfile( blog ) || Boolean( editing[ blog.id ] );
}

/** The switches to send: only the rows that show one; the server keeps the rest. */
export function switchesPayload( blogs, switches ) {
const out = {};
for ( const blog of blogs ) {
if ( hasSavedProfile( blog ) && blog.id in switches ) {
out[ blog.id ] = switches[ blog.id ];
}
}
return out;
}

/** The selects' values, as the network route takes them. */
Expand All @@ -35,6 +54,17 @@ export function mappingPayload( values ) {
return out;
}

/** The switches as they start: each site's saved on/off. */
export function initialSwitches( blogs ) {
const out = {};
for ( const blog of blogs ) {
if ( 'enabled' in blog ) {
out[ blog.id ] = blog.enabled;
}
}
return out;
}

/** The values the selects start on. */
export function initialMapping( blogs ) {
const out = {};
Expand Down
109 changes: 83 additions & 26 deletions js/network/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@
* network's sites, so every load shows both as they are now; POST saves.
*/
import { createRoot, useEffect, useState } from '@wordpress/element';
import { Button, Notice, SelectControl, Spinner, TextControl, ToggleControl } from '@wordpress/components';
import { Button, FormToggle, Notice, SelectControl, Spinner, TextControl, ToggleControl } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';

import { secretHint, secretsPayload } from '../lib/secrets';
import { CHANGED, initialMapping, mappingPayload, profileLabel, profilesFor, rowStatus } from '../lib/mapping';
import { hasSavedProfile, initialMapping, initialSwitches, isEditing, mappingPayload, profileLabel, profilesFor, savedLabel, switchesPayload } from '../lib/mapping';
import '../settings/style.css';

const NETWORK = '/owa-wp/v1/network';

function SitesTable( { payload, mapping, setMapping } ) {
function SitesTable( { payload, mapping, setMapping, switches, setSwitches, editing, setEditing } ) {
const fetched = payload.sitesFetchedAt
? `Profiles last fetched from OWA: ${ new Date( payload.sitesFetchedAt * 1000 ).toISOString().slice( 0, 16 ).replace( 'T', ' ' ) } UTC (${ payload.profiles.length }).`
: 'No Profiles have been fetched from OWA yet.';
? `Observation Profiles last fetched from OWA: ${ new Date( payload.sitesFetchedAt * 1000 ).toISOString().slice( 0, 16 ).replace( 'T', ' ' ) } UTC (${ payload.profiles.length }).`
: 'No Observation Profiles have been fetched from OWA yet.';

return (
<>
Expand All @@ -27,25 +27,33 @@ function SitesTable( { payload, mapping, setMapping } ) {
<thead>
<tr>
<th>Site</th>
<th>Website ID</th>
{ payload.assignProfiles && <th>Status</th> }
<th>Observation Profile</th>
{ payload.assignProfiles && <th className="owa-wp-tracking">Tracking</th> }
</tr>
</thead>
<tbody>
{ payload.blogs.map( ( blog ) => {
if ( ! payload.assignProfiles ) {
return (
<tr key={ blog.id }>
<tr key={ blog.id } data-host={ blog.host }>
<td>{ blog.host }</td>
<td>{ blog.own || 'not chosen yet' }</td>
</tr>
);
}

const value = mapping[ blog.id ] ?? '';
const status = rowStatus( blog, value );
// the switch (and greying) only once the row has a saved Profile
const saved = hasSavedProfile( blog );
const enabled = switches[ blog.id ] ?? true;
const off = saved && ! enabled;
const edit = isEditing( blog, editing );

const setValue = ( v ) => setMapping( ( current ) => ( { ...current, [ blog.id ]: v } ) );
const setEdit = ( on ) => setEditing( ( current ) => ( { ...current, [ blog.id ]: on } ) );

const options = [
{ label: '— not tracked —', value: '' },
{ label: 'Select Profile…', value: '' },
...profilesFor( payload.profiles, blog.baseDomain ).map( ( profile ) => ( { label: profileLabel( profile ), value: profile.id } ) ),
];

Expand All @@ -54,20 +62,54 @@ function SitesTable( { payload, mapping, setMapping } ) {
}

return (
<tr key={ blog.id }>
<td>{ blog.host }</td>
<tr key={ blog.id } data-host={ blog.host } className={ off ? 'is-disabled' : '' }>
<td>
{ blog.host }
{ blog.saved === null && <span className="owa-wp-badge">New</span> }
</td>
<td>
<SelectControl
__next40pxDefaultSize
__nextHasNoMarginBottom
label={ `Website ID for ${ blog.host }` }
hideLabelFromVision
value={ value }
options={ options }
onChange={ ( v ) => setMapping( ( current ) => ( { ...current, [ blog.id ]: v } ) ) }
/>
{ edit ? (
<div className="owa-wp-profile">
<SelectControl
__next40pxDefaultSize
__nextHasNoMarginBottom
label={ `Observation Profile for ${ blog.host }` }
hideLabelFromVision
value={ value }
options={ options }
disabled={ off }
onChange={ setValue }
/>
{ saved && (
<Button
variant="link"
onClick={ () => {
setValue( blog.saved );
setEdit( false );
} }
>
Cancel
</Button>
) }
</div>
) : (
<div className="owa-wp-profile">
<span className="owa-wp-saved-profile">{ savedLabel( payload.profiles, blog.saved ) }</span>
<Button variant="link" disabled={ off } onClick={ () => setEdit( true ) } aria-label={ `Edit the Observation Profile for ${ blog.host }` }>
Edit
</Button>
</div>
) }
</td>
<td className="owa-wp-tracking">
{ saved && (
<FormToggle
aria-label={ `Track ${ blog.host }` }
checked={ enabled }
onChange={ () => setSwitches( ( current ) => ( { ...current, [ blog.id ]: ! enabled } ) ) }
/>
) }
</td>
<td className={ `owa-wp-status${ status === CHANGED ? ' is-changed' : '' }` }>{ status }</td>
</tr>
);
} ) }
Expand All @@ -83,6 +125,8 @@ function NetworkPage() {
const [ assign, setAssign ] = useState( false );
const [ password, setPassword ] = useState( {} );
const [ mapping, setMapping ] = useState( {} );
const [ switches, setSwitches ] = useState( {} );
const [ editing, setEditing ] = useState( {} );
const [ errors, setErrors ] = useState( {} );
const [ notices, setNotices ] = useState( [] );
const [ busy, setBusy ] = useState( false );
Expand All @@ -94,6 +138,8 @@ function NetworkPage() {
setAssign( result.assignProfiles );
setPassword( {} );
setMapping( initialMapping( result.blogs ) );
setSwitches( initialSwitches( result.blogs ) );
setEditing( {} );
setErrors( problems );
setNotices( [
...( result.saved ? [ { status: 'success', text: 'Settings saved.' } ] : [] ),
Expand All @@ -118,6 +164,7 @@ function NetworkPage() {

if ( payload.assignProfiles && assign ) {
data.assignedSiteIds = mappingPayload( mapping );
data.enabledSites = switchesPayload( payload.blogs, switches );
}

setBusy( true );
Expand Down Expand Up @@ -165,7 +212,7 @@ function NetworkPage() {
__next40pxDefaultSize
__nextHasNoMarginBottom
label="Application Password"
help="An application password from OWA (My Preferences > Application Passwords). Site admins can list the Profiles its OWA user can see, so use a read-only password of a user limited to this network's Properties."
help="An application password from OWA (My Preferences > Application Passwords). Site admins can list the Observation Profiles its OWA user can see, so use a read-only password of a user limited to this network's Properties."
type="password"
autoComplete="off"
value={ password.value || '' }
Expand All @@ -185,17 +232,27 @@ function NetworkPage() {
<div className="owa-wp-field">
<ToggleControl
__nextHasNoMarginBottom
label="Network configures each site's Profile (site admins cannot change it)"
label="Network configures each site's Observation Profile (site admins cannot change it)"
help={
assign !== payload.assignProfiles
? 'Save to switch.'
: "Each site starts on the Profile whose domain is exactly the site's domain. Change any site below and save."
: "Each site starts on the Observation Profile whose domain is exactly the site's domain. Change any site below and save."
}
checked={ assign }
onChange={ setAssign }
/>
</div>
{ payload.active && <SitesTable payload={ payload } mapping={ mapping } setMapping={ setMapping } /> }
{ payload.active && (
<SitesTable
payload={ payload }
mapping={ mapping }
setMapping={ setMapping }
switches={ switches }
setSwitches={ setSwitches }
editing={ editing }
setEditing={ setEditing }
/>
) }
<p className="owa-wp-actions">
<Button variant="primary" onClick={ save } isBusy={ busy } disabled={ busy }>
Save Changes
Expand Down
26 changes: 17 additions & 9 deletions js/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
} from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';

import { visibleFields, visibleSections } from '../lib/fields';
import { fieldText, setupNotices, showTagSettingsLink, visibleFields, visibleSections } from '../lib/fields';
import { secretHint, secretsPayload } from '../lib/secrets';
import './style.css';

Expand All @@ -30,26 +30,28 @@ function FieldError( { message } ) {
return message ? <p className="owa-wp-field-error">{ message }</p> : null;
}

function WebsiteId( { field, value, onChange, sites, payload } ) {
const options = [ { label: sites.loading ? 'Loading…' : 'Select…', value: '' }, ...sites.choices ];
function WebsiteId( { field, value, onChange, sites, payload, values } ) {
const v2 = values.owaVersion === '2';
const text = fieldText( field, values.owaVersion );
const options = [ { label: sites.loading ? 'Loading…' : v2 ? 'Select Profile…' : 'Select…', value: '' }, ...sites.choices ];

return (
<>
<SelectControl
__next40pxDefaultSize
__nextHasNoMarginBottom
label={ field.title }
label={ text.title }
value={ value }
options={ options }
onChange={ onChange }
disabled={ field.readonly || sites.loading }
help={ field.readonly ? '' : field.description }
help={ field.readonly ? '' : text.description }
/>
{ sites.notice && <p className="owa-wp-field-error">{ sites.notice }</p> }
{ payload.network.note && <p className="description">{ payload.network.note }</p> }
{ payload.tagSettingsUrl && (
{ showTagSettingsLink( payload, values ) && (
<p>
<ExternalLink href={ payload.tagSettingsUrl }>Configure this Website ID&apos;s tracking tag in OWA</ExternalLink>
<ExternalLink href={ payload.tagSettingsUrl }>Configure this Observation Profile&apos;s tracking tag in OWA</ExternalLink>
</p>
) }
</>
Expand All @@ -58,7 +60,8 @@ function WebsiteId( { field, value, onChange, sites, payload } ) {

function Field( { field, payload, values, setValue, secrets, setSecret, sites, error } ) {
const value = values[ field.key ];
const common = { label: field.title, help: field.description };
const text = fieldText( field, values.owaVersion );
const common = { label: text.title, help: text.description };

switch ( field.type ) {
case 'radio':
Expand Down Expand Up @@ -96,7 +99,7 @@ function Field( { field, payload, values, setValue, secrets, setSecret, sites, e
}

case 'select':
return <WebsiteId field={ field } value={ value } onChange={ ( v ) => setValue( field.key, v ) } sites={ sites } payload={ payload } />;
return <WebsiteId field={ field } value={ value } onChange={ ( v ) => setValue( field.key, v ) } sites={ sites } payload={ payload } values={ values } />;

default:
return (
Expand Down Expand Up @@ -179,6 +182,11 @@ function SettingsPage() {
{ notice.text }
</Notice>
) }
{ setupNotices( payload, values ).map( ( text ) => (
<Notice key={ text } status="warning" isDismissible={ false }>
{ text }
</Notice>
) ) }
<p>Settings for integrating WordPress with an existing Open Web Analytics (OWA) server instance.</p>
<TabPanel tabs={ tabs } key={ tabs.map( ( tab ) => tab.name ).join() }>
{ ( tab ) => (
Expand Down
Loading
Loading