diff --git a/.circleci/deploy-exclude.txt b/.circleci/deploy-exclude.txt
index 0248edd7..5e9f300f 100755
--- a/.circleci/deploy-exclude.txt
+++ b/.circleci/deploy-exclude.txt
@@ -10,5 +10,4 @@
node_modules
package.json
package-lock.json
-src
webpack.config.js
diff --git a/.circleci/deploy.sh b/.circleci/deploy.sh
index 7edb89b3..0a33223a 100755
--- a/.circleci/deploy.sh
+++ b/.circleci/deploy.sh
@@ -62,11 +62,6 @@ fi
rsync -av "$SRC_DIR/" "$BUILD_DIR" --exclude-from "$SRC_DIR/.circleci/deploy-exclude.txt"
-# Swap commit placeholders
-SCRIPT_HASH=$(md5sum build/analytics.js | awk '{ print $1 }')
-find . -name '*.php' | xargs sed -i.bak 's/__SCRIPT_HASH__/'"$SCRIPT_HASH"'/g'
-find . -name '*.bak' -delete
-
# Add changed files
git add .
diff --git a/.eslintrc b/.eslintrc
index 2fb769cb..970e504e 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,7 +1,8 @@
{
"extends": "humanmade",
"globals": {
- "Altis": "readonly"
+ "Altis": "readonly",
+ "wp": "readonly"
},
"rules": {
"no-multi-str": "off",
diff --git a/README.md b/README.md
index 5d06c88a..368664f5 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,10 @@ Similar to `registerAttribute()` above but for metrics.
Allows you to define the Elasticsearch server URL directly.
+**`ALTIS_ANALYTICS_LOG_QUERIES`**
+
+Define as true to enable logging queries to the error log.
+
### Filters
The plugin provides a few hooks for you to control the default endpoint data and attributes recorded with events.
@@ -131,6 +135,9 @@ A user session covers every event recorded between opening the website and closi
- `ModelVersion`: Browser version.
- `Platform`: The device operating system.
- `PlatformVersion`: The operating system version.
+ - `Location`
+ - `Country`: The endpoint's country if known / available.
+ - `City`: The endpoint's city if known or available.
- `User`
- `UserAttributes`
- Any custom attributes associated with the user if known.
@@ -224,6 +231,28 @@ The output will look something like the following:
You can further trim the size of the returned response using the `filter_path` query parameter. For example if we're only interested in the stats aggregation we can set `filter_path=-aggregations.sessions` to remove it from the response.
+## Audiences
+
+Audiences are user-defined categories of users, based on conditions related to their analytics data.
+
+Audiences allow for the creation of conditions to narrow down event queries or endpoints but also can be used for determining effects on the client side.
+
+### Mapping Event Data
+
+To enable the use of any event record data in the audience editor it needs to be mapped to a human readable label using the `Altis\Analytics\Audiences\register_field()` function:
+
+```php
+use function Altis\Analytics\Audiences\register_field;
+
+add_action( 'init', function () {
+ register_field( 'endpoint.Location.City', __( 'City' ) );
+} );
+```
+
+In the above example the 1st parameter `endpoint.Location.City` represents the field in the event record to query against. Other examples include `attributes.utm_campaign` or `endpoint.User.UserAttibrutes.custom` for example.
+
+The 2nd parameter is a human readable label for the audience field.
+
## Required Infrastructure
A specific infrastructure set up is required to use this plugin:
diff --git a/composer.json b/composer.json
index baa21d74..c76ef065 100644
--- a/composer.json
+++ b/composer.json
@@ -3,7 +3,8 @@
"description": "AWS Pinpoint Analytics integration for WordPress",
"type": "wordpress-plugin",
"require": {
- "aws/aws-sdk-php": "^3.103.0"
+ "aws/aws-sdk-php": "^3.103.0",
+ "johnbillion/extended-cpts": "^4.3.2"
},
"license": "GPL-2.0-or-later",
"authors": [
diff --git a/inc/audiences/namespace.php b/inc/audiences/namespace.php
new file mode 100644
index 00000000..4aca3d46
--- /dev/null
+++ b/inc/audiences/namespace.php
@@ -0,0 +1,757 @@
+ false,
+ 'show_ui' => true,
+ 'supports' => [
+ 'title',
+ 'excerpt',
+ ],
+ 'menu_icon' => 'dashicons-groups',
+ 'menu_position' => 151,
+ 'show_in_rest' => true,
+ 'rest_base' => 'audiences',
+ 'hierarchical' => false,
+ 'admin_cols' => [
+ 'active' => [
+ 'title' => __( 'Status', 'altis-analytics' ),
+ 'function' => __NAMESPACE__ . '\\render_status_column',
+ ],
+ 'estimate' => [
+ 'title' => __( 'Size', 'altis-analytics' ),
+ 'function' => __NAMESPACE__ . '\\estimate_ui',
+ ],
+ 'last_modified' => [
+ 'title' => __( 'Last Modified', 'altis-analytics' ),
+ 'post_field' => 'post_modified',
+ ],
+ ],
+ ],
+ [
+ 'singular' => __( 'Audience', 'altis-analytics' ),
+ 'plural' => __( 'Audiences', 'altis-analytics' ),
+ ]
+ );
+}
+
+/**
+ * Set up default event data mappings.
+ */
+function register_default_event_data_maps() {
+ register_field( 'attributes.referer', __( 'Referrer', 'altis-analytics' ) );
+ register_field( 'endpoint.Demographic.Model', __( 'Browser', 'altis-analytics' ) );
+ register_field( 'endpoint.Demographic.ModelVersion', __( 'Browser version', 'altis-analytics' ) );
+ register_field( 'endpoint.Demographic.Locale', __( 'Browser Locale', 'altis-analytics' ) );
+ register_field( 'endpoint.Demographic.Platform', __( 'Operating system', 'altis-analytics' ) );
+ register_field( 'endpoint.Demographic.PlatformVersion', __( 'Operating system version', 'altis-analytics' ) );
+ register_field( 'endpoint.Location.Country', __( 'Country', 'altis-analytics' ) );
+}
+
+/**
+ * Remove built-in metaboxes from the Audiences edit page.
+ */
+function adjust_meta_boxes() {
+ remove_meta_box( 'submitdiv', POST_TYPE, 'side' );
+ remove_meta_box( 'slugdiv', POST_TYPE, 'normal' );
+ remove_meta_box( 'postexcerpt', POST_TYPE, 'normal' );
+}
+
+/**
+ * Temporarily hide the title field.
+ *
+ * Removes post type support on the edit screen temporarily, then readds as
+ * soon as the UI no longer cares.
+ */
+function hide_title_field( WP_Post $post ) {
+ if ( $post->post_type !== POST_TYPE ) {
+ return;
+ }
+
+ remove_post_type_support( POST_TYPE, 'title' );
+
+ $callback = function () use ( &$callback ) {
+ add_post_type_support( POST_TYPE, 'title' );
+ remove_action( 'edit_form_after_title', $callback );
+ };
+ add_action( 'edit_form_after_title', $callback );
+}
+
+/**
+ * Render the "Status" column for an audience.
+ */
+function render_status_column() : void {
+ if ( get_post_status() === 'publish' ) {
+ esc_html_e( 'Active', 'altis-analytics' );
+ } else {
+ esc_html_e( 'Inactive', 'altis-analytics' );
+ }
+}
+
+/**
+ * Add Audience UI placeholder.
+ *
+ * @param WP_Post $post
+ */
+function audience_ui( WP_Post $post ) {
+ if ( $post->post_type !== POST_TYPE ) {
+ return;
+ }
+
+ printf(
+ '
' .
+ '
%s
' .
+ '
' .
+ '
',
+ $post->ID,
+ esc_attr( wp_json_encode( get_audience( $post->ID ) ) ),
+ esc_attr( wp_json_encode( get_field_data() ) ),
+ esc_html__( 'Loading...', 'altis-analytics' ),
+ esc_html__( 'Javascript is required to use the audience editor.', 'altis-analytics' )
+ );
+
+ wp_nonce_field( 'altis-analytics', 'altis_analytics_nonce' );
+}
+
+/**
+ * Add estimate UI placeholder.
+ *
+ * @param WP_Post $post
+ */
+function estimate_ui( WP_Post $post = null ) {
+ // Use current post if none passed.
+ if ( ! $post ) {
+ $post = get_post();
+ }
+
+ if ( $post->post_type !== POST_TYPE ) {
+ return;
+ }
+
+ $audience = get_audience( $post->ID );
+
+ printf(
+ '' .
+ '
%s
' .
+ '
' .
+ '
',
+ esc_attr( wp_json_encode( $audience ) ),
+ esc_html__( 'Loading...', 'altis-analytics' ),
+ // translators: %d is the number of visitors matching the audience
+ sprintf( esc_html__( '%d visitors in the last 7 days', 'altis-analytics' ), $audience['count'] ?? 0 )
+ );
+}
+
+/**
+ * Support saving the audience configuration the old school way.
+ *
+ * @param int $post_id The current audience post ID.
+ */
+function save_post( $post_id ) {
+ if ( ! isset( $_POST['altis_analytics_nonce'] ) ) {
+ return;
+ }
+
+ if ( ! wp_verify_nonce( $_POST['altis_analytics_nonce'], 'altis-analytics' ) ) {
+ return;
+ }
+
+ if ( ! isset( $_POST['audience'] ) ) {
+ return;
+ }
+
+ if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
+ return;
+ }
+
+ if ( ! is_array( $_POST['audience'] ) ) {
+ return;
+ }
+
+ save_audience( $post_id, $_POST['audience'] );
+}
+
+/**
+ * Saves an audience config to the given post ID.
+ *
+ * @param integer $post_id The post ID to save the audience to.
+ * @param array $audience The audience configuration.
+ * @return bool True on successful update or false on failure.
+ */
+function save_audience( int $post_id, array $audience ) : bool {
+ // Clear errors.
+ delete_post_meta( $post_id, 'audience_error' );
+
+ // Validate using audience schema.
+ $valid = rest_validate_value_from_schema( $audience, REST_API\get_audience_schema(), 'audience' );
+
+ if ( is_wp_error( $valid ) ) {
+ update_post_meta( $post_id, 'audience_error', wp_slash( $valid->get_error_message() ) );
+ return false;
+ }
+
+ // Save the audience configuration.
+ return (bool) update_post_meta( $post_id, 'audience', wp_slash( $audience ) );
+}
+
+/**
+ * Remove quick edit inline action.
+ *
+ * @param array $actions Inline actions array.
+ * @param WP_Post $post The current post.
+ * @return array
+ */
+function remove_quick_edit( array $actions, WP_Post $post ) : array {
+ if ( $post->post_type !== POST_TYPE ) {
+ return $actions;
+ }
+
+ unset( $actions['inline hide-if-no-js'] );
+ return $actions;
+}
+
+/**
+ * Removes the quick edit bulk action.
+ *
+ * @param array $actions Bulk actions array.
+ * @return array
+ */
+function remove_bulk_actions( array $actions ) : array {
+ unset( $actions['edit'] );
+ return $actions;
+}
+
+/**
+ * Get the audience configuration data.
+ *
+ * @param int $post_id
+ * @return array|null
+ */
+function get_audience( int $post_id ) : ?array {
+ return wp_unslash( get_post_meta( $post_id, 'audience', true ) ?: null );
+}
+
+/**
+ * Get list of mapped analytics data fields.
+ *
+ * @return array
+ */
+function get_fields() : array {
+ global $altis_analytics_event_data_maps;
+ return array_values( $altis_analytics_event_data_maps ?: [] );
+}
+
+/**
+ * Adds an elasticsearch to field data mapping.
+ *
+ * @param string $field The elasticsearch field name.
+ * @param string $label A human readable label for the field.
+ */
+function register_field( string $field, string $label ) {
+ global $altis_analytics_event_data_maps;
+ if ( empty( $altis_analytics_event_data_maps ) ) {
+ $altis_analytics_event_data_maps = [];
+ }
+
+ $altis_analytics_event_data_maps[ $field ] = [
+ 'name' => $field,
+ 'label' => $label,
+ 'type' => Utils\get_field_type( $field ),
+ ];
+ ksort( $altis_analytics_event_data_maps );
+}
+
+/**
+ * Queue up the audience admin UI scripts.
+ */
+function admin_enqueue_scripts() {
+ if ( get_current_screen()->post_type !== POST_TYPE ) {
+ return;
+ }
+
+ wp_dequeue_script( 'post' );
+
+ wp_enqueue_script(
+ 'altis-analytics-audience-ui',
+ Utils\get_asset_url( 'audiences.js' ),
+ [
+ 'lodash',
+ 'react',
+ 'react-dom',
+ 'wp-i18n',
+ 'wp-hooks',
+ 'wp-data',
+ 'wp-components',
+ 'wp-api-fetch',
+ ],
+ null,
+ true
+ );
+
+ wp_enqueue_style( 'wp-components' );
+
+ // Hydrate with data to speed up the front end.
+ $data = [
+ 'Fields' => get_field_data(),
+ ];
+
+ // Add post data server side to load front end quickly on legacy edit screens.
+ if ( isset( $_GET['post'] ) ) {
+ $response = rest_do_request( sprintf( '/wp/v2/audiences/%d', $_GET['post'] ) );
+ $data['Current'] = $response->get_data();
+ }
+
+ wp_add_inline_script(
+ 'altis-analytics-audience-ui',
+ sprintf(
+ 'window.Altis = window.Altis || {};' .
+ 'window.Altis.Analytics = window.Altis.Analytics || {};' .
+ 'window.Altis.Analytics.BuildURL = %s;' .
+ 'window.Altis.Analytics.Audiences = %s;',
+ wp_json_encode( plugins_url( 'build', dirname( __FILE__, 2 ) ) ),
+ wp_json_encode( (object) $data )
+ ),
+ 'before'
+ );
+
+ wp_enqueue_style(
+ 'altis-analytics-audience-ui',
+ plugins_url( 'src/audiences/index.css', dirname( __FILE__, 2 ) ),
+ [],
+ '2020-03-19-1'
+ );
+}
+
+/**
+ * Get estimated audience size.
+ *
+ * @param array $audience
+ * @return void
+ */
+function get_estimate( array $audience ) : ?array {
+ $query = [
+ 'query' => [
+ 'bool' => [
+ 'filter' => [
+ // Set current site.
+ [
+ 'term' => [
+ 'attributes.blogId.keyword' => get_current_blog_id(),
+ ],
+ ],
+
+ // Last 7 days.
+ [
+ 'range' => [
+ 'event_timestamp' => [
+ 'gte' => Utils\milliseconds() - ( WEEK_IN_SECONDS * 1000 ),
+ ],
+ ],
+ ],
+
+ // Limit event type to pageView.
+ [
+ 'term' => [
+ 'event_type.keyword' => 'pageView',
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'aggs' => [
+ 'estimate' => [
+ 'cardinality' => [
+ 'field' => 'endpoint.Id.keyword',
+ ],
+ ],
+ 'histogram' => [
+ 'histogram' => [
+ 'field' => 'event_timestamp',
+ 'interval' => 6 * HOUR_IN_SECONDS * 1000, // 6 hour chunks.
+ 'extended_bounds' => [
+ 'min' => Utils\milliseconds() - ( WEEK_IN_SECONDS * 1000 ),
+ 'max' => Utils\milliseconds(),
+ ],
+ ],
+ ],
+ ],
+ 'size' => 0,
+ 'sort' => [
+ 'event_timestamp' => 'desc',
+ ],
+ ];
+
+ // Append the groups query.
+ $query['query']['bool']['filter'][] = build_audience_query( $audience );
+
+ $key = sprintf( 'estimate:%s', sha1( serialize( $audience ) ) );
+ $cache = wp_cache_get( $key, 'altis-audiences' );
+ if ( $cache ) {
+ return $cache;
+ }
+
+ $result = Utils\query( $query );
+
+ if ( ! $result ) {
+ return $result;
+ }
+
+ $histogram = array_map( function ( array $bucket ) {
+ return [
+ 'index' => $bucket['key'],
+ 'count' => $bucket['doc_count'],
+ ];
+ }, $result['aggregations']['histogram']['buckets'] );
+
+ $estimate = [
+ 'count' => $result['aggregations']['estimate']['value'],
+ 'total' => get_unique_endpoint_count(),
+ 'histogram' => array_values( $histogram ),
+ ];
+
+ wp_cache_set( $key, $estimate, 'altis-audiences', HOUR_IN_SECONDS );
+
+ return $estimate;
+}
+
+/**
+ * Get total unique endpoints for the past 7 days.
+ *
+ * @return integer|null
+ */
+function get_unique_endpoint_count() : ?int {
+ $query = [
+ 'query' => [
+ 'bool' => [
+ 'filter' => [
+ // Set current site.
+ [
+ 'term' => [
+ 'attributes.blogId.keyword' => get_current_blog_id(),
+ ],
+ ],
+
+ // Last 7 days.
+ [
+ 'range' => [
+ 'event_timestamp' => [
+ 'gte' => Utils\milliseconds() - ( WEEK_IN_SECONDS * 1000 ),
+ ],
+ ],
+ ],
+
+ // Limit event type to pageView.
+ [
+ 'term' => [
+ 'event_type.keyword' => 'pageView',
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'aggs' => [
+ 'count' => [
+ 'cardinality' => [
+ 'field' => 'endpoint.Id.keyword',
+ ],
+ ],
+ ],
+ 'size' => 0,
+ 'sort' => [
+ 'event_timestamp' => 'desc',
+ ],
+ ];
+
+ $cache = wp_cache_get( 'total-uniques', 'altis-audiences' );
+ if ( $cache ) {
+ return $cache;
+ }
+
+ $result = Utils\query( $query );
+
+ if ( ! $result ) {
+ return $result;
+ }
+
+ $count = intval( $result['aggregations']['count']['value'] );
+
+ wp_cache_set( 'total-uniques', $count, 'altis-audiences', HOUR_IN_SECONDS );
+
+ return $count;
+}
+
+/**
+ * Get a list of mapped fields and some information on the available
+ * data for those fields.
+ *
+ * Example results:
+ *
+ * [
+ * [
+ * 'name' => 'endpoint.Location.Country',
+ * 'label' => 'Country',
+ * 'type' => 'string',
+ * 'data' => [
+ * [ 'key' => 'GB', 'doc_count' => 281 ],
+ * [ 'key' => 'US', 'doc_count' => 127 ]
+ * ]
+ * ],
+ * [
+ * 'name' => 'metrics.UserSpend',
+ * 'label' => 'Total User Spend',
+ * 'type' => 'number',
+ * 'stats' => [
+ * 'sum' => 560,
+ * 'min' => 10,
+ * 'max' => 210,
+ * 'avg' => 35
+ * ]
+ * ]
+ * ]
+ *
+ * @return array|null
+ */
+function get_field_data() : ?array {
+ $maps = get_fields();
+
+ $key = sprintf( 'fields:%s', sha1( serialize( wp_list_pluck( $maps, 'name' ) ) ) );
+ $cache = wp_cache_get( $key, 'altis-audiences' );
+ if ( $cache ) {
+ return $cache;
+ }
+
+ $query = [
+ 'query' => [
+ 'bool' => [
+ 'filter' => [
+ // Query for current site.
+ [
+ 'term' => [
+ 'attributes.blogId.keyword' => (string) get_current_blog_id(),
+ ],
+ ],
+
+ // Last 7 days.
+ [
+ 'range' => [
+ 'event_timestamp' => [
+ 'gte' => Utils\milliseconds() - ( WEEK_IN_SECONDS * 1000 ),
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'size' => 0,
+ 'aggs' => [],
+ 'sort' => [
+ 'event_timestamp' => 'desc',
+ ],
+ ];
+
+ foreach ( $maps as $map ) {
+ // For numeric fields get a simple stats aggregation.
+ if ( Utils\get_field_type( $map['name'] ) === 'number' ) {
+ $query['aggs'][ $map['name'] ] = [
+ 'stats' => [
+ 'field' => $map['name'],
+ ],
+ ];
+ }
+ // Default to terms aggregations for top 100 different values available for each field.
+ if ( Utils\get_field_type( $map['name'] ) === 'string' ) {
+ $query['aggs'][ $map['name'] ] = [
+ 'terms' => [
+ 'field' => "{$map['name']}.keyword",
+ 'size' => 100,
+ ],
+ ];
+ }
+ }
+
+ $result = Utils\query( $query );
+
+ if ( ! $result ) {
+ return $result;
+ }
+
+ $aggregations = $result['aggregations'];
+
+ // Normalise aggregations to useful just the useful data.
+ $fields = [];
+ foreach ( $maps as $field ) {
+ $field_name = $field['name'];
+ if ( isset( $aggregations[ $field_name ]['buckets'] ) ) {
+ $field['data'] = array_map( function ( $bucket ) {
+ return [
+ 'value' => $bucket['key'],
+ 'count' => $bucket['doc_count'],
+ ];
+ }, $aggregations[ $field_name ]['buckets'] );
+ } else {
+ $field['stats'] = $aggregations[ $field_name ];
+ }
+
+ $fields[] = $field;
+ }
+
+ // Cache the data.
+ wp_cache_set( $key, $fields, 'altis-audiences', HOUR_IN_SECONDS );
+
+ return $fields;
+}
+
+/**
+ * Convert an audience config array to an Elasticsearch query.
+ * The response is designed to be used within a bool filter query eg:
+ *
+ * $query = [
+ * 'query' => [
+ * 'bool' => [
+ * 'filter' => [
+ * get_filter_query( $audience ),
+ * ],
+ * ],
+ * ],
+ * ];
+ *
+ * @param array $audience
+ * @return array
+ */
+function build_audience_query( array $audience ) : array {
+ // Map the include values to elasticsearch query filters.
+ $include_map = [
+ 'any' => 'should',
+ 'all' => 'filter',
+ 'none' => 'must_not',
+ ];
+
+ $group_queries = [];
+
+ foreach ( $audience['groups'] as $group ) {
+ $group_include = $include_map[ $group['include'] ];
+ $group_query = [
+ 'bool' => [
+ $group_include => [],
+ ],
+ ];
+
+ foreach ( $group['rules'] as $rule ) {
+ $rule_query = [
+ 'bool' => [
+ 'filter' => [],
+ 'must_not' => [],
+ ],
+ ];
+
+ // Handle string comparisons.
+ if ( Utils\get_field_type( $rule['field'] ) === 'string' ) {
+ switch ( $rule['operator'] ) {
+ case '=':
+ $rule_query['bool']['filter'][] = [
+ 'term' => [
+ "{$rule['field']}.keyword" => $rule['value'],
+ ],
+ ];
+ break;
+ case '!=':
+ $rule_query['bool']['must_not'][] = [
+ 'term' => [
+ "{$rule['field']}.keyword" => $rule['value'],
+ ],
+ ];
+ break;
+ case '*=':
+ $rule_query['bool']['filter'][] = [
+ 'wildcard' => [
+ "{$rule['field']}.keyword" => "*{$rule['value']}*",
+ ],
+ ];
+ break;
+ case '!*':
+ $rule_query['bool']['must_not'][] = [
+ 'wildcard' => [
+ "{$rule['field']}.keyword" => "*{$rule['value']}*",
+ ],
+ ];
+ break;
+ case '^=':
+ $rule_query['bool']['filter'][] = [
+ 'wildcard' => [
+ "{$rule['field']}.keyword" => "{$rule['value']}*",
+ ],
+ ];
+ break;
+ }
+ }
+
+ // Handle numeric field comparisons.
+ if ( Utils\get_field_type( $rule['field'] ) === 'number' ) {
+ $rule_query['bool']['filter'][] = [
+ 'range' => [ $rule['field'] => [ $rule['operator'] => intval( $rule['value'] ) ] ],
+ ];
+ }
+
+ // Add the rule query to the group.
+ $group_query['bool'][ $group_include ][] = $rule_query;
+ }
+
+ // Add the group query to the list of group queries.
+ $group_queries[] = $group_query;
+ }
+
+ $groups_include = $include_map[ $audience['include'] ];
+ $groups_query = [
+ 'bool' => [
+ $groups_include => $group_queries,
+ ],
+ ];
+
+ return $groups_query;
+}
diff --git a/inc/audiences/rest_api/namespace.php b/inc/audiences/rest_api/namespace.php
new file mode 100644
index 00000000..bf814998
--- /dev/null
+++ b/inc/audiences/rest_api/namespace.php
@@ -0,0 +1,267 @@
+ WP_REST_Server::READABLE,
+ 'callback' => 'Altis\\Analytics\\Audiences\\get_field_data',
+ 'permission_callback' => __NAMESPACE__ . '\\check_edit_permission',
+ ],
+ 'schema' => [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'name' => [
+ 'type' => 'string',
+ 'required' => true,
+ ],
+ 'label' => [
+ 'type' => 'string',
+ 'required' => true,
+ ],
+ 'type' => [
+ 'type' => 'string',
+ 'required' => true,
+ ],
+ 'data' => [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'value' => [ 'type' => 'string' ],
+ 'count' => [ 'type' => 'number' ],
+ ],
+ ],
+ ],
+ 'stats' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'count' => [ 'type' => 'number' ],
+ 'min' => [ 'type' => 'number' ],
+ 'max' => [ 'type' => 'number' ],
+ 'avg' => [ 'type' => 'number' ],
+ 'sum' => [ 'type' => 'number' ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ] );
+
+ // Fetch an audience size estimate.
+ register_rest_route( 'analytics/v1', 'audiences/estimate', [
+ [
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => __NAMESPACE__ . '\\handle_estimate_request',
+ 'permission_callback' => __NAMESPACE__ . '\\check_edit_permission',
+ 'args' => [
+ 'audience' => [
+ 'description' => __( 'A URL encoded audience configuration JSON string', 'altis-analytics' ),
+ 'required' => true,
+ 'type' => 'string',
+ 'validate_callback' => __NAMESPACE__ . '\\validate_estimate_audience',
+ 'sanitize_callback' => __NAMESPACE__ . '\\sanitize_estimate_audience',
+ ],
+ ],
+ ],
+ 'schema' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'count' => [ 'type' => 'number' ],
+ 'total' => [ 'type' => 'number' ],
+ 'histogram' => [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'index' => [ 'type' => 'string' ],
+ 'count' => [ 'type' => 'number' ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ] );
+
+ // Handle the audience configuration data retrieval and saving via the REST API.
+ register_rest_field( Audiences\POST_TYPE, 'audience', [
+ 'get_callback' => function ( array $post ) {
+ return Audiences\get_audience( $post['id'] );
+ },
+ 'update_callback' => function ( $value, WP_Post $post ) {
+ return Audiences\save_audience( $post->ID, (array) $value );
+ },
+ 'schema' => get_audience_schema(),
+ ] );
+}
+
+/**
+ * Get the JSON schema for the rule object.
+ *
+ * @return array
+ */
+function get_rule_schema() : array {
+ return [
+ 'type' => 'object',
+ 'properties' => [
+ 'field' => [
+ 'type' => 'string',
+ ],
+ 'operator' => [
+ 'type' => 'string',
+ 'enum' => Audiences\COMPARISON_OPERATORS,
+ ],
+ 'value' => [
+ 'type' => [
+ 'string',
+ 'number',
+ ],
+ ],
+ ],
+ ];
+}
+
+/**
+ * Get the JSON schema for the group object.
+ *
+ * @return array
+ */
+function get_group_schema() : array {
+ return [
+ 'type' => 'object',
+ 'properties' => [
+ 'include' => [
+ 'type' => 'string',
+ 'enum' => [
+ 'any',
+ 'all',
+ 'none',
+ ],
+ ],
+ 'rules' => [
+ 'type' => 'array',
+ 'items' => get_rule_schema(),
+ ],
+ ],
+ ];
+}
+
+/**
+ * Get the JSON schema for the audience configuration object.
+ *
+ * @return array
+ */
+function get_audience_schema() : array {
+ return [
+ 'type' => 'object',
+ 'properties' => [
+ 'include' => [
+ 'type' => 'string',
+ 'enum' => [
+ 'any',
+ 'all',
+ 'none',
+ ],
+ ],
+ 'groups' => [
+ 'type' => 'array',
+ 'items' => get_group_schema(),
+ ],
+ ],
+ ];
+}
+
+/**
+ * Retrieve the estimate response.
+ *
+ * @param WP_REST_Request $request
+ * @return WP_REST_Response
+ */
+function handle_estimate_request( WP_REST_Request $request ) : WP_REST_Response {
+ $audience = $request->get_param( 'audience' );
+ $estimate = Audiences\get_estimate( $audience );
+ return rest_ensure_response( $estimate );
+}
+
+/**
+ * Validate the estimate audience configuration parameter.
+ *
+ * @param string $param The audience configuration JSON string.
+ * @return bool
+ */
+function validate_estimate_audience( $param ) {
+ $audience = json_decode( $param, true );
+
+ if ( json_last_error() !== JSON_ERROR_NONE ) {
+ return new WP_Error(
+ 'altis_audience_estimate_json_invalid',
+ sprintf(
+ /* translators: %s: JSON error message */
+ __( 'Could not decode JSON: %s', 'altis-analytics' ),
+ json_last_error_msg()
+ )
+ );
+ }
+
+ // Validate against the audience schema after decoding.
+ return rest_validate_value_from_schema( $audience, get_audience_schema(), 'audience' );
+}
+
+/**
+ * Sanitize the estimate audience value.
+ *
+ * @param string $param The audience configuration JSON string.
+ * @return array|WP_Error
+ */
+function sanitize_estimate_audience( $param ) {
+ $audience = json_decode( $param, true );
+
+ if ( json_last_error() !== JSON_ERROR_NONE ) {
+ return new WP_Error(
+ 'altis_audience_estimate_json_invalid',
+ sprintf(
+ /* translators: %s: JSON error message */
+ __( 'Could not decode JSON: %s', 'altis-analytics' ),
+ json_last_error_msg()
+ )
+ );
+ }
+
+ return $audience;
+}
+
+/**
+ * Check user can edit audience posts.
+ *
+ * @return bool
+ */
+function check_edit_permission() : bool {
+ $type = get_post_type_object( Audiences\POST_TYPE );
+ return current_user_can( $type->cap->edit_posts );
+}
diff --git a/inc/namespace.php b/inc/namespace.php
index 018e616e..0aae0e98 100644
--- a/inc/namespace.php
+++ b/inc/namespace.php
@@ -3,12 +3,15 @@
* Altis Analytics.
*
* @package altis-analytics
- *
*/
namespace Altis\Analytics;
+use function Altis\Analytics\Utils\get_asset_url;
+
function setup() {
+ // Setup audiences.
+ Audiences\setup();
// Handle async scripts.
add_filter( 'script_loader_tag', __NAMESPACE__ . '\\async_scripts', 20, 2 );
// Load analytics scripts super early.
@@ -157,7 +160,7 @@ function async_scripts( string $tag, string $handle ) : string {
function enqueue_scripts() {
global $wp_scripts;
- wp_enqueue_script( 'altis-analytics', plugins_url( 'build/analytics.js', __DIR__ ), [], '__SCRIPT_HASH__', false );
+ wp_enqueue_script( 'altis-analytics', get_asset_url( 'analytics.js' ), [], null, false );
wp_add_inline_script(
'altis-analytics',
sprintf(
diff --git a/inc/utils/namespace.php b/inc/utils/namespace.php
index 936a96a0..bc26dd9d 100644
--- a/inc/utils/namespace.php
+++ b/inc/utils/namespace.php
@@ -5,6 +5,38 @@
namespace Altis\Analytics\Utils;
+use const Altis\Analytics\ROOT_DIR;
+
+/**
+ * Return asset file name based on generated manifest.json file.
+ *
+ * @param string $filename
+ * @return string|false
+ */
+function get_asset_url( string $filename ) {
+ $manifest_file = ROOT_DIR . '/build/manifest.json';
+
+ if ( ! file_exists( $manifest_file ) ) {
+ return false;
+ }
+
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+ $manifest = file_get_contents( $manifest_file );
+ $manifest = json_decode( $manifest, true );
+
+ if ( ! $manifest || ! isset( $manifest[ $filename ] ) ) {
+ return false;
+ }
+
+ $path = $manifest[ $filename ];
+
+ if ( strpos( $path, 'http' ) !== false ) {
+ return $path;
+ }
+
+ return plugins_url( $manifest[ $filename ], ROOT_DIR . '/build/assets' );
+}
+
/**
* Calculate the combined standard deviation for multiple groups of
* averages, standard deviations and sizes.
@@ -81,12 +113,19 @@ function get_elasticsearch_url() : string {
*
* @param array $query A full elasticsearch Query DSL array.
* @param array $params URL query parameters to append to request URL.
+ * @param string $path The endpoint to query against, defaults to _search.
* @return array|null
*/
-function query( array $query, array $params = [] ) : ?array {
+function query( array $query, array $params = [], string $path = '_search' ) : ?array {
+
+ // Sanitize path.
+ $path = trim( $path, '/' );
// Get URL.
- $url = add_query_arg( $params, get_elasticsearch_url() . '/analytics*/_search' );
+ $url = add_query_arg( $params, get_elasticsearch_url() . '/analytics*/' . $path );
+
+ // Escape the URL to ensure nothing strange was passed in via $path.
+ $url = esc_url_raw( $url );
$response = wp_remote_post( $url, [
'headers' => [
@@ -97,16 +136,24 @@ function query( array $query, array $params = [] ) : ?array {
if ( wp_remote_retrieve_response_code( $response ) !== 200 || is_wp_error( $response ) ) {
if ( is_wp_error( $response ) ) {
- trigger_error( sprintf(
- 'Analytics: elasticsearch query failed: %s',
- $response->get_error_message()
- ), E_USER_WARNING );
+ trigger_error(
+ sprintf(
+ "Analytics: elasticsearch query failed:\n%s\n%s",
+ $url,
+ $response->get_error_message()
+ ),
+ E_USER_WARNING
+ );
} else {
- trigger_error( sprintf(
- "Analytics: elasticsearch query failed:\n%s\n%s",
- json_encode( $query ),
- wp_remote_retrieve_body( $response )
- ), E_USER_WARNING );
+ trigger_error(
+ sprintf(
+ "Analytics: elasticsearch query failed:\n%s\n%s\n%s",
+ $url,
+ json_encode( $query ),
+ wp_remote_retrieve_body( $response )
+ ),
+ E_USER_WARNING
+ );
}
return null;
}
@@ -119,6 +166,18 @@ function query( array $query, array $params = [] ) : ?array {
return null;
}
+ // Enable logging for analytics queries.
+ if ( defined( 'ALTIS_ANALYTICS_LOG_QUERIES' ) && ALTIS_ANALYTICS_LOG_QUERIES ) {
+ error_log(
+ sprintf(
+ "Analytics: elasticsearch query:\n%s\n%s\n%s",
+ $url,
+ json_encode( $query ),
+ wp_remote_retrieve_body( $response )
+ )
+ );
+ }
+
return $result;
}
@@ -223,3 +282,40 @@ function merge_aggregates( array $current, array $new, string $bucket_type = ''
return $merged;
}
+
+/**
+ * Determine type of Elasticsearch field by name.
+ *
+ * @param string $field The full field name.
+ * @return string|null $type One of 'string', 'number' or 'date'.
+ */
+function get_field_type( string $field ) : ?string {
+ if ( empty( $field ) ) {
+ return null;
+ }
+
+ $numeric_fields = [
+ 'event_timestamp',
+ 'arrival_timestamp',
+ 'session.start_timestamp',
+ 'session.stop_timestamp',
+ ];
+
+ $is_numeric_field = in_array( $field, $numeric_fields, true );
+ $is_metric = stripos( $field, 'metrics' ) !== false;
+
+ if ( $is_numeric_field || $is_metric ) {
+ return 'number';
+ }
+
+ $date_fields = [
+ 'endpoint.CreationDate',
+ 'endpoint.EffectiveDate',
+ ];
+
+ if ( in_array( $field, $date_fields, true ) ) {
+ return 'date';
+ }
+
+ return 'string';
+}
diff --git a/package-lock.json b/package-lock.json
index 443ee806..0a31c98f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -571,6 +571,95 @@
"semver": "^5.5.0"
}
},
+ "@babel/helper-create-class-features-plugin": {
+ "version": "7.8.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.6.tgz",
+ "integrity": "sha512-klTBDdsr+VFFqaDHm5rR69OpEQtO2Qv8ECxHS1mNhJJvaHArR6a1xTf5K/eZW7eZpJbhCx3NW1Yt/sKsLXLblg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-function-name": "^7.8.3",
+ "@babel/helper-member-expression-to-functions": "^7.8.3",
+ "@babel/helper-optimise-call-expression": "^7.8.3",
+ "@babel/helper-plugin-utils": "^7.8.3",
+ "@babel/helper-replace-supers": "^7.8.6",
+ "@babel/helper-split-export-declaration": "^7.8.3"
+ },
+ "dependencies": {
+ "@babel/generator": {
+ "version": "7.9.4",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.4.tgz",
+ "integrity": "sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.9.0",
+ "jsesc": "^2.5.1",
+ "lodash": "^4.17.13",
+ "source-map": "^0.5.0"
+ }
+ },
+ "@babel/helper-replace-supers": {
+ "version": "7.8.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz",
+ "integrity": "sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-member-expression-to-functions": "^7.8.3",
+ "@babel/helper-optimise-call-expression": "^7.8.3",
+ "@babel/traverse": "^7.8.6",
+ "@babel/types": "^7.8.6"
+ }
+ },
+ "@babel/parser": {
+ "version": "7.9.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz",
+ "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==",
+ "dev": true
+ },
+ "@babel/traverse": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.0.tgz",
+ "integrity": "sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.8.3",
+ "@babel/generator": "^7.9.0",
+ "@babel/helper-function-name": "^7.8.3",
+ "@babel/helper-split-export-declaration": "^7.8.3",
+ "@babel/parser": "^7.9.0",
+ "@babel/types": "^7.9.0",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0",
+ "lodash": "^4.17.13"
+ }
+ },
+ "@babel/types": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.0.tgz",
+ "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.9.0",
+ "lodash": "^4.17.13",
+ "to-fast-properties": "^2.0.0"
+ }
+ },
+ "debug": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ }
+ }
+ },
"@babel/helper-create-regexp-features-plugin": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.3.tgz",
@@ -715,6 +804,12 @@
"@babel/types": "^7.8.3"
}
},
+ "@babel/helper-validator-identifier": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz",
+ "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==",
+ "dev": true
+ },
"@babel/helper-wrap-function": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz",
@@ -761,6 +856,16 @@
"@babel/plugin-syntax-async-generators": "^7.8.0"
}
},
+ "@babel/plugin-proposal-class-properties": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz",
+ "integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-create-class-features-plugin": "^7.8.3",
+ "@babel/helper-plugin-utils": "^7.8.3"
+ }
+ },
"@babel/plugin-proposal-dynamic-import": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz",
@@ -1381,6 +1486,192 @@
"to-fast-properties": "^2.0.0"
}
},
+ "@emotion/cache": {
+ "version": "10.0.29",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz",
+ "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==",
+ "requires": {
+ "@emotion/sheet": "0.9.4",
+ "@emotion/stylis": "0.8.5",
+ "@emotion/utils": "0.11.3",
+ "@emotion/weak-memoize": "0.2.5"
+ }
+ },
+ "@emotion/core": {
+ "version": "10.0.28",
+ "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.28.tgz",
+ "integrity": "sha512-pH8UueKYO5jgg0Iq+AmCLxBsvuGtvlmiDCOuv8fGNYn3cowFpLN98L8zO56U0H1PjDIyAlXymgL3Wu7u7v6hbA==",
+ "requires": {
+ "@babel/runtime": "^7.5.5",
+ "@emotion/cache": "^10.0.27",
+ "@emotion/css": "^10.0.27",
+ "@emotion/serialize": "^0.11.15",
+ "@emotion/sheet": "0.9.4",
+ "@emotion/utils": "0.11.3"
+ }
+ },
+ "@emotion/css": {
+ "version": "10.0.27",
+ "resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz",
+ "integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==",
+ "requires": {
+ "@emotion/serialize": "^0.11.15",
+ "@emotion/utils": "0.11.3",
+ "babel-plugin-emotion": "^10.0.27"
+ }
+ },
+ "@emotion/hash": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
+ "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
+ },
+ "@emotion/is-prop-valid": {
+ "version": "0.8.7",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.7.tgz",
+ "integrity": "sha512-OPkKzUeiid0vEKjZqnGcy2mzxjIlCffin+L2C02pdz/bVlt5zZZE2VzO0D3XOPnH0NEeF21QNKSXiZphjr4xiQ==",
+ "requires": {
+ "@emotion/memoize": "0.7.4"
+ }
+ },
+ "@emotion/memoize": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
+ "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
+ },
+ "@emotion/serialize": {
+ "version": "0.11.16",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz",
+ "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==",
+ "requires": {
+ "@emotion/hash": "0.8.0",
+ "@emotion/memoize": "0.7.4",
+ "@emotion/unitless": "0.7.5",
+ "@emotion/utils": "0.11.3",
+ "csstype": "^2.5.7"
+ }
+ },
+ "@emotion/sheet": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz",
+ "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA=="
+ },
+ "@emotion/stylis": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
+ "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
+ },
+ "@emotion/unitless": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
+ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
+ },
+ "@emotion/utils": {
+ "version": "0.11.3",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz",
+ "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw=="
+ },
+ "@emotion/weak-memoize": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz",
+ "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
+ },
+ "@types/anymatch": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
+ "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA=="
+ },
+ "@types/events": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
+ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g=="
+ },
+ "@types/glob": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
+ "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
+ "requires": {
+ "@types/events": "*",
+ "@types/minimatch": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/minimatch": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
+ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA=="
+ },
+ "@types/node": {
+ "version": "13.9.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.2.tgz",
+ "integrity": "sha512-bnoqK579sAYrQbp73wwglccjJ4sfRdKU7WNEZ5FW4K2U6Kc0/eZ5kvXG0JKsEKFB50zrFmfFt52/cvBbZa7eXg=="
+ },
+ "@types/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
+ },
+ "@types/source-list-map": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
+ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA=="
+ },
+ "@types/tapable": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.5.tgz",
+ "integrity": "sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ=="
+ },
+ "@types/uglify-js": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz",
+ "integrity": "sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==",
+ "requires": {
+ "source-map": "^0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "@types/webpack": {
+ "version": "4.41.7",
+ "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.7.tgz",
+ "integrity": "sha512-OQG9viYwO0V1NaNV7d0n79V+n6mjOV30CwgFPIfTzwmk8DHbt+C4f2aBGdCYbo3yFyYD6sjXfqqOjwkl1j+ulA==",
+ "requires": {
+ "@types/anymatch": "*",
+ "@types/node": "*",
+ "@types/tapable": "*",
+ "@types/uglify-js": "*",
+ "@types/webpack-sources": "*",
+ "source-map": "^0.6.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "@types/webpack-sources": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.6.tgz",
+ "integrity": "sha512-FtAWR7wR5ocJ9+nP137DV81tveD/ZgB1sadnJ/axUGM3BUVfRPx8oQNMtv3JNfTeHx3VP7cXiyfR/jmtEsVHsQ==",
+ "requires": {
+ "@types/node": "*",
+ "@types/source-list-map": "*",
+ "source-map": "^0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
"@webassemblyjs/ast": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
@@ -1758,6 +2049,19 @@
"is-string": "^1.0.5"
}
},
+ "array-union": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+ "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+ "requires": {
+ "array-uniq": "^1.0.1"
+ }
+ },
+ "array-uniq": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+ "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY="
+ },
"array-unique": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
@@ -1878,6 +2182,49 @@
"object.assign": "^4.1.0"
}
},
+ "babel-plugin-emotion": {
+ "version": "10.0.29",
+ "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.29.tgz",
+ "integrity": "sha512-7Jpi1OCxjyz0k163lKtqP+LHMg5z3S6A7vMBfHnF06l2unmtsOmFDzZBpGf0CWo1G4m8UACfVcDJiSiRuu/cSw==",
+ "requires": {
+ "@babel/helper-module-imports": "^7.0.0",
+ "@emotion/hash": "0.8.0",
+ "@emotion/memoize": "0.7.4",
+ "@emotion/serialize": "^0.11.16",
+ "babel-plugin-macros": "^2.0.0",
+ "babel-plugin-syntax-jsx": "^6.18.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^1.0.5",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7"
+ }
+ },
+ "babel-plugin-macros": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz",
+ "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==",
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "cosmiconfig": "^6.0.0",
+ "resolve": "^1.12.0"
+ }
+ },
+ "babel-plugin-styled-components": {
+ "version": "1.10.7",
+ "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.7.tgz",
+ "integrity": "sha512-MBMHGcIA22996n9hZRf/UJLVVgkEOITuR2SvjHLb5dSTUyR4ZRGn+ngITapes36FI3WLxZHfRhkA1ffHxihOrg==",
+ "requires": {
+ "@babel/helper-annotate-as-pure": "^7.0.0",
+ "@babel/helper-module-imports": "^7.0.0",
+ "babel-plugin-syntax-jsx": "^6.18.0",
+ "lodash": "^4.17.11"
+ }
+ },
+ "babel-plugin-syntax-jsx": {
+ "version": "6.18.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
+ "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
+ },
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -2182,14 +2529,18 @@
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
+ "camelize": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
+ "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
+ },
"caniuse-lite": {
"version": "1.0.30001027",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001027.tgz",
@@ -2278,6 +2629,15 @@
}
}
},
+ "clean-webpack-plugin": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz",
+ "integrity": "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A==",
+ "requires": {
+ "@types/webpack": "^4.4.31",
+ "del": "^4.1.1"
+ }
+ },
"cli-cursor": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
@@ -2481,6 +2841,36 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
+ "cosmiconfig": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
+ "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
+ "requires": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.1.0",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.7.2"
+ },
+ "dependencies": {
+ "parse-json": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz",
+ "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==",
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1",
+ "lines-and-columns": "^1.1.6"
+ }
+ },
+ "path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="
+ }
+ }
+ },
"create-ecdh": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
@@ -2545,6 +2935,26 @@
"randomfill": "^1.0.3"
}
},
+ "css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
+ },
+ "css-to-react-native": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
+ "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
+ "requires": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
+ "csstype": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.9.tgz",
+ "integrity": "sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q=="
+ },
"cyclist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
@@ -2630,6 +3040,20 @@
}
}
},
+ "del": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz",
+ "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==",
+ "requires": {
+ "@types/glob": "^7.1.1",
+ "globby": "^6.1.0",
+ "is-path-cwd": "^2.0.0",
+ "is-path-in-cwd": "^2.0.0",
+ "p-map": "^2.0.0",
+ "pify": "^4.0.1",
+ "rimraf": "^2.6.3"
+ }
+ },
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -2673,6 +3097,14 @@
"esutils": "^2.0.2"
}
},
+ "dom-helpers": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz",
+ "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==",
+ "requires": {
+ "@babel/runtime": "^7.1.2"
+ }
+ },
"domain-browser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
@@ -2694,6 +3126,11 @@
"stream-shift": "^1.0.0"
}
},
+ "dynamic-public-path-webpack-plugin": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/dynamic-public-path-webpack-plugin/-/dynamic-public-path-webpack-plugin-1.0.4.tgz",
+ "integrity": "sha1-5UUbG3AOhF8qQAnd96mo7byZRSc="
+ },
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -2780,7 +3217,6 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
- "dev": true,
"requires": {
"is-arrayish": "^0.2.1"
}
@@ -2789,7 +3225,6 @@
"version": "1.17.4",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz",
"integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==",
- "dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
@@ -2808,7 +3243,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
- "dev": true,
"requires": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
@@ -3451,6 +3885,11 @@
"pkg-dir": "^3.0.0"
}
},
+ "find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
+ },
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
@@ -3528,6 +3967,16 @@
"readable-stream": "^2.0.0"
}
},
+ "fs-extra": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
"fs-readdir-recursive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
@@ -4148,6 +4597,25 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
},
+ "globby": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+ "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
+ "requires": {
+ "array-union": "^1.0.1",
+ "glob": "^7.0.3",
+ "object-assign": "^4.0.1",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
+ }
+ }
+ },
"graceful-fs": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
@@ -4166,7 +4634,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dev": true,
"requires": {
"function-bind": "^1.1.1"
}
@@ -4238,6 +4705,14 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
+ "hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "requires": {
+ "react-is": "^16.7.0"
+ }
+ },
"homedir-polyfill": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -4309,7 +4784,6 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
"integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
- "dev": true,
"requires": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -4429,8 +4903,7 @@
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
- "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
- "dev": true
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
"is-binary-path": {
"version": "1.0.1",
@@ -4448,8 +4921,7 @@
"is-callable": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
- "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
- "dev": true
+ "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q=="
},
"is-data-descriptor": {
"version": "0.1.4",
@@ -4472,8 +4944,7 @@
"is-date-object": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
- "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==",
- "dev": true
+ "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
},
"is-descriptor": {
"version": "0.1.6",
@@ -4534,6 +5005,27 @@
}
}
},
+ "is-path-cwd": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
+ "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ=="
+ },
+ "is-path-in-cwd": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz",
+ "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==",
+ "requires": {
+ "is-path-inside": "^2.1.0"
+ }
+ },
+ "is-path-inside": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz",
+ "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==",
+ "requires": {
+ "path-is-inside": "^1.0.2"
+ }
+ },
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -4552,7 +5044,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
"integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
- "dev": true,
"requires": {
"has": "^1.0.3"
}
@@ -4572,7 +5063,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
- "dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
@@ -4647,12 +5137,20 @@
},
"dependencies": {
"minimist": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
- "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
}
}
},
+ "jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+ "requires": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
"jsx-ast-utils": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz",
@@ -4699,6 +5197,11 @@
"type-check": "~0.3.2"
}
},
+ "lines-and-columns": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
+ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
+ },
"load-json-file": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
@@ -4743,9 +5246,9 @@
}
},
"minimist": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
- "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
}
}
},
@@ -4839,6 +5342,11 @@
"p-is-promise": "^2.0.0"
}
},
+ "memoize-one": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
+ "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
+ },
"memory-fs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@@ -5159,8 +5667,7 @@
"object-inspect": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
- "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==",
- "dev": true
+ "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw=="
},
"object-keys": {
"version": "1.1.1",
@@ -5190,7 +5697,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz",
"integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==",
- "dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.1",
@@ -5334,6 +5840,11 @@
"p-limit": "^2.0.0"
}
},
+ "p-map": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
+ "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="
+ },
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
@@ -5358,7 +5869,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
"requires": {
"callsites": "^3.0.0"
}
@@ -5423,8 +5933,7 @@
"path-is-inside": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
- "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
- "dev": true
+ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM="
},
"path-key": {
"version": "2.0.1",
@@ -5475,6 +5984,19 @@
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
},
+ "pinkie": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+ "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA="
+ },
+ "pinkie-promise": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+ "requires": {
+ "pinkie": "^2.0.0"
+ }
+ },
"pkg-dir": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz",
@@ -5488,6 +6010,11 @@
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs="
},
+ "postcss-value-parser": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz",
+ "integrity": "sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg=="
+ },
"prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
@@ -5597,6 +6124,16 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
+ "query-string": {
+ "version": "6.11.1",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.11.1.tgz",
+ "integrity": "sha512-1ZvJOUl8ifkkBxu2ByVM/8GijMIPx+cef7u3yroO3Ogm4DOdZcF5dcrWTIlSHe3Pg/mtlt6/eFjObDfJureZZA==",
+ "requires": {
+ "decode-uri-component": "^0.2.0",
+ "split-on-first": "^1.0.0",
+ "strict-uri-encode": "^2.0.0"
+ }
+ },
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
@@ -5661,11 +6198,58 @@
"scheduler": "^0.18.0"
}
},
+ "react-input-autosize": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz",
+ "integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==",
+ "requires": {
+ "prop-types": "^15.5.8"
+ }
+ },
"react-is": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
},
+ "react-lifecycles-compat": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
+ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
+ },
+ "react-select": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/react-select/-/react-select-3.0.8.tgz",
+ "integrity": "sha512-v9LpOhckLlRmXN5A6/mGGEft4FMrfaBFTGAnuPHcUgVId7Je42kTq9y0Z+Ye5z8/j0XDT3zUqza8gaRaI1PZIg==",
+ "requires": {
+ "@babel/runtime": "^7.4.4",
+ "@emotion/cache": "^10.0.9",
+ "@emotion/core": "^10.0.9",
+ "@emotion/css": "^10.0.9",
+ "memoize-one": "^5.0.0",
+ "prop-types": "^15.6.0",
+ "react-input-autosize": "^2.2.2",
+ "react-transition-group": "^2.2.1"
+ }
+ },
+ "react-sparklines": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/react-sparklines/-/react-sparklines-1.7.0.tgz",
+ "integrity": "sha512-bJFt9K4c5Z0k44G8KtxIhbG+iyxrKjBZhdW6afP+R7EnIq+iKjbWbEFISrf3WKNFsda+C46XAfnX0StS5fbDcg==",
+ "requires": {
+ "prop-types": "^15.5.10"
+ }
+ },
+ "react-transition-group": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz",
+ "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==",
+ "requires": {
+ "dom-helpers": "^3.4.0",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2",
+ "react-lifecycles-compat": "^3.0.4"
+ }
+ },
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@@ -5912,8 +6496,7 @@
"resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
},
"resolve-url": {
"version": "0.2.1",
@@ -6108,6 +6691,11 @@
"safe-buffer": "^5.0.1"
}
},
+ "shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -6324,6 +6912,11 @@
"integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
"dev": true
},
+ "split-on-first": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
+ "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="
+ },
"split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@@ -6405,6 +6998,11 @@
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
},
+ "strict-uri-encode": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
+ "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY="
+ },
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
@@ -6444,7 +7042,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz",
"integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==",
- "dev": true,
"requires": {
"define-properties": "^1.1.3",
"function-bind": "^1.1.1"
@@ -6454,7 +7051,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz",
"integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==",
- "dev": true,
"requires": {
"define-properties": "^1.1.3",
"function-bind": "^1.1.1"
@@ -6500,6 +7096,23 @@
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"dev": true
},
+ "styled-components": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.0.1.tgz",
+ "integrity": "sha512-E0xKTRIjTs4DyvC1MHu/EcCXIj6+ENCP8hP01koyoADF++WdBUOrSGwU1scJRw7/YaYOhDvvoad6VlMG+0j53A==",
+ "requires": {
+ "@babel/helper-module-imports": "^7.0.0",
+ "@babel/traverse": "^7.4.5",
+ "@emotion/is-prop-valid": "^0.8.3",
+ "@emotion/stylis": "^0.8.4",
+ "@emotion/unitless": "^0.7.4",
+ "babel-plugin-styled-components": ">= 1",
+ "css-to-react-native": "^3.0.0",
+ "hoist-non-react-statics": "^3.0.0",
+ "shallowequal": "^1.1.0",
+ "supports-color": "^5.5.0"
+ }
+ },
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -6768,6 +7381,11 @@
"imurmurhash": "^0.1.4"
}
},
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
+ },
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -7014,6 +7632,17 @@
}
}
},
+ "webpack-manifest-plugin": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-2.2.0.tgz",
+ "integrity": "sha512-9S6YyKKKh/Oz/eryM1RyLVDVmy3NSPV0JXMRhZ18fJsq+AwGxUY34X54VNwkzYcEmEkDwNxuEOboCZEebJXBAQ==",
+ "requires": {
+ "fs-extra": "^7.0.0",
+ "lodash": ">=3.5 <5",
+ "object.entries": "^1.1.0",
+ "tapable": "^1.0.0"
+ }
+ },
"webpack-sources": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz",
@@ -7030,6 +7659,14 @@
}
}
},
+ "webpack-subresource-integrity": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-1.4.0.tgz",
+ "integrity": "sha512-GB1kB/LwAWC3CxwcedGhMkxGpNZxSheCe1q+KJP1bakuieAdX/rGHEcf5zsEzhKXpqsGqokgsDoD9dIkr61VDQ==",
+ "requires": {
+ "webpack-sources": "^1.3.0"
+ }
+ },
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -7135,6 +7772,14 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
+ "yaml": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.7.2.tgz",
+ "integrity": "sha512-qXROVp90sb83XtAoqE8bP9RwAkTTZbugRUTm5YeFCBfNRPEp2YzTeqWiz7m5OORHzEvrA/qcGS8hp/E+MMROYw==",
+ "requires": {
+ "@babel/runtime": "^7.6.3"
+ }
+ },
"yargs": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz",
diff --git a/package.json b/package.json
index 78c41d11..b7b0cb22 100644
--- a/package.json
+++ b/package.json
@@ -24,13 +24,22 @@
"@babel/runtime": "^7.8.4",
"@wordpress/babel-preset-default": "^4.10.0",
"babel-loader": "^8.0.6",
+ "query-string": "^6.11.1",
+ "react-select": "^3.0.8",
+ "react-sparklines": "^1.7.0",
+ "styled-components": "^5.0.1",
"deepmerge": "^4.2.2",
"ua-parser-js": "^0.7.21",
"webpack": "^4.41.6",
"webpack-bundle-analyzer": "^3.6.0",
- "webpack-cli": "^3.3.11"
+ "webpack-cli": "^3.3.11",
+ "dynamic-public-path-webpack-plugin": "^1.0.4",
+ "webpack-subresource-integrity": "^1.4.0",
+ "webpack-manifest-plugin": "^2.2.0",
+ "clean-webpack-plugin": "^3.0.0"
},
"devDependencies": {
+ "@babel/plugin-proposal-class-properties": "^7.8.3",
"babel-eslint": "^10.1.0",
"eslint": "^5.16.0",
"eslint-config-humanmade": "^0.8.0",
diff --git a/plugin.php b/plugin.php
index dcac8e89..8f2ecd74 100644
--- a/plugin.php
+++ b/plugin.php
@@ -18,7 +18,9 @@
require_once ROOT_DIR . '/vendor/autoload.php';
}
-require_once 'inc/namespace.php';
-require_once 'inc/utils/namespace.php';
+require_once __DIR__ . '/inc/namespace.php';
+require_once __DIR__ . '/inc/audiences/namespace.php';
+require_once __DIR__ . '/inc/audiences/rest_api/namespace.php';
+require_once __DIR__ . '/inc/utils/namespace.php';
add_action( 'plugins_loaded', __NAMESPACE__ . '\\setup' );
diff --git a/src/audiences/edit/components/audience-editor.js b/src/audiences/edit/components/audience-editor.js
new file mode 100644
index 00000000..87ea46e9
--- /dev/null
+++ b/src/audiences/edit/components/audience-editor.js
@@ -0,0 +1,109 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import Group from './group';
+import SelectInclude from './select-include';
+import {
+ defaultAudience,
+ defaultGroup,
+} from '../data/defaults';
+
+const { __ } = wp.i18n;
+const { Button } = wp.components;
+
+const StyledAudienceEditor = styled.div`
+ margin: 0 0 40px;
+
+ .audience-editor__include {
+ margin: 20px 0;
+ }
+`;
+
+export default class AudienceEditor extends Component {
+ onChangeInclude = e => {
+ this.props.onChange( {
+ ...this.props.audience,
+ include: e.target.value,
+ } );
+ }
+
+ onAddGroup = () => {
+ this.props.onChange( {
+ ...this.props.audience,
+ groups: [
+ ...this.props.audience.groups,
+ defaultGroup,
+ ],
+ } );
+ }
+
+ onUpdateGroup = ( id, group = {} ) => {
+ const { audience } = this.props;
+ this.props.onChange( {
+ ...audience,
+ groups: [
+ ...audience.groups.slice( 0, id ),
+ {
+ ...audience.groups[ id ],
+ ...group,
+ },
+ ...audience.groups.slice( id + 1 ),
+ ],
+ } );
+ }
+
+ onRemoveGroup = id => {
+ const { audience } = this.props;
+ this.props.onChange( {
+ ...audience,
+ groups: [
+ ...audience.groups.slice( 0, id ),
+ ...audience.groups.slice( id + 1 ),
+ ],
+ } );
+ }
+
+ render() {
+ const { audience } = this.props;
+
+ return (
+
+
+
+
+
+ { audience.groups.map( ( group, groupId ) => (
+ 1 }
+ namePrefix={ `audience[groups][${ groupId }]` }
+ title={ `${ __( 'Group' ) } ${ groupId + 1 }` }
+ onChange={ value => this.onUpdateGroup( groupId, value ) }
+ onRemove={ () => this.onRemoveGroup( groupId ) }
+ { ...group }
+ />
+ ) ) }
+
+
+
+ );
+ }
+}
+
+AudienceEditor.defaultProps = {
+ audience: defaultAudience,
+ fields: [],
+ onChange: () => {},
+};
diff --git a/src/audiences/edit/components/estimate.js b/src/audiences/edit/components/estimate.js
new file mode 100644
index 00000000..f36d6f9d
--- /dev/null
+++ b/src/audiences/edit/components/estimate.js
@@ -0,0 +1,91 @@
+import React from 'react';
+import { Sparklines, SparklinesLine } from 'react-sparklines';
+import styled from 'styled-components';
+
+import PieChart from './pie-chart';
+
+const { useSelect } = wp.data;
+const { __ } = wp.i18n;
+
+const StyledEstimate = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0;
+
+ .audience-estimate__title {
+ flex: 0 0 100%;
+ margin: 0 0 20px;
+ }
+
+ .audience-estimate__percentage {
+ flex: 0 1 100px;
+ margin-right: 20px;
+ }
+
+ .audience-estimate__totals {
+ flex: 2;
+ }
+
+ .audience-estimate__totals svg {
+ max-width: 220px;
+ margin-top: 5px;
+ margin-bottom: 10px;
+ }
+
+ .audience-estimate__totals p {
+ margin: 0;
+ }
+
+ .audience-estimate__totals strong {
+ margin-right: 2px;
+ }
+`;
+
+export default function Estimate( props ) {
+ const {
+ audience,
+ sparkline,
+ title,
+ } = props;
+
+ if ( ! audience ) {
+ return null;
+ }
+
+ const estimate = useSelect( select => select( 'audience' ).getEstimate( audience ), [ audience ] );
+ const percent = estimate.total ? Math.round( ( estimate.count / estimate.total ) * 100 ) : 0;
+
+ return (
+
+ { title && (
+ { title }
+ ) }
+
+
+ { sparkline && (
+
item.count ) }
+ preserveAspectRatio="xMidYMid meet"
+ >
+
+
+ ) }
+
+ { estimate.count }
+ { ' ' }
+ { __( 'uniques in the last 7 days' ) }
+
+
+
+ );
+}
+
+Estimate.defaultProps = {
+ audience: null,
+ sparkline: false,
+ title: '',
+};
diff --git a/src/audiences/edit/components/group.js b/src/audiences/edit/components/group.js
new file mode 100644
index 00000000..a18afdf1
--- /dev/null
+++ b/src/audiences/edit/components/group.js
@@ -0,0 +1,135 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import Rule from './rule';
+import SelectInclude from './select-include';
+import { defaultRule } from '../data/defaults';
+
+const { __ } = wp.i18n;
+const { Button } = wp.components;
+
+const StyledGroup = styled.div`
+ background: rgba(0, 0, 0, 0.02);
+ border-radius: 3px;
+ border: 1px solid rgba(0, 0, 0, .1);
+ padding: 20px;
+ margin: 0 0 20px;
+
+ .audience-editor__group-header {
+ margin: 0 0 15px;
+ display: flex;
+ align-items: baseline;
+ }
+
+ h3 {
+ margin: 0 20px 0 0;
+ text-transform: lowercase;
+ font-variant: small-caps;
+ }
+
+ .audience-editor__group-footer .components-button {
+ margin-right: 10px;
+ }
+`;
+
+export default class Group extends Component {
+ onAddRule = () => {
+ this.props.onChange( {
+ rules: [
+ ...this.props.rules,
+ defaultRule,
+ ],
+ } );
+ };
+
+ onChangeRule = e => {
+ this.props.onChange( {
+ include: e.target.value,
+ } );
+ };
+
+ onRemoveRule = id => {
+ const { rules } = this.props;
+ this.props.onChange( {
+ rules: [
+ ...rules.slice( 0, id ),
+ ...rules.slice( id + 1 ),
+ ],
+ } );
+ };
+
+ updateRule = ( ruleId, rule ) => {
+ const { rules } = this.props;
+ this.props.onChange( {
+ rules: [
+ ...rules.slice( 0, ruleId ),
+ {
+ ...rules[ ruleId ],
+ ...rule,
+ },
+ ...rules.slice( ruleId + 1 ),
+ ],
+ } );
+ };
+
+ render() {
+ const {
+ canRemove,
+ include,
+ fields,
+ namePrefix,
+ rules,
+ title,
+ onRemove,
+ } = this.props;
+
+ return (
+
+
+ { title && (
+
{ title }
+ ) }
+
+
+
+ { rules.map( ( rule, ruleId ) => (
+ 1 }
+ fields={ fields }
+ namePrefix={ `${ namePrefix }[rules][${ ruleId }]` }
+ onChange={ value => this.updateRule( ruleId, value ) }
+ onRemove={ () => this.onRemoveRule( ruleId ) }
+ { ...rule }
+ />
+ ) ) }
+
+
+
+
+ { canRemove && (
+
+ ) }
+
+
+ );
+ }
+}
diff --git a/src/audiences/edit/components/pie-chart.js b/src/audiences/edit/components/pie-chart.js
new file mode 100644
index 00000000..6c06f370
--- /dev/null
+++ b/src/audiences/edit/components/pie-chart.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const StyledPie = styled.svg`
+ circle {
+ fill: transparent;
+ stroke: ${ props => props.stroke || 'rgb(0, 124, 186)' };
+ stroke-linecap: round;
+ stroke-opacity: 0.2;
+ stroke-dasharray: 101 100;
+ stroke-dashoffset: 0;
+ stroke-width: 3;
+ transform: rotate(-90deg);
+ transform-origin: center;
+ transition: stroke-dasharray 0.3s ease-in-out;
+ }
+ circle[data-percent] {
+ stroke-dasharray: ${ props => props.percent === 100 ? 101 : props.percent } 100;
+ stroke-opacity: 1;
+ }
+ text {
+ stroke-width: 0;
+ font-size: 9px;
+ }
+`;
+
+export default function PieChart( props ) {
+ return (
+
+
+
+ { props.percent }%
+
+ );
+}
+
+PieChart.defaultProps = {
+ percent: 0,
+};
diff --git a/src/audiences/edit/components/rule.js b/src/audiences/edit/components/rule.js
new file mode 100644
index 00000000..d4a29e42
--- /dev/null
+++ b/src/audiences/edit/components/rule.js
@@ -0,0 +1,168 @@
+import React from 'react';
+import styled from 'styled-components';
+
+import SelectOperator from './select-operator';
+
+const { Button } = wp.components;
+const { useSelect } = wp.data;
+const { __ } = wp.i18n;
+
+const StyledRule = styled.div`
+ margin: 0 0 15px;
+ display: flex;
+
+ select, input {
+ flex: 1;
+ }
+
+ && > * + * {
+ margin-left: 5px;
+ }
+
+ .audience-editor__rule-operator,
+ button {
+ flex 0;
+ }
+`;
+
+const RuleInput = props => {
+ const {
+ disabled,
+ currentField,
+ name,
+ operator,
+ value,
+ onChange,
+ } = props;
+
+ switch ( currentField.type ) {
+ case 'number':
+ return (
+
+ );
+
+ case 'string':
+ default:
+ switch ( operator ) {
+ case '=':
+ case '!=':
+ return (
+
+ );
+
+ case '*=':
+ case '!*':
+ case '^=':
+ return (
+
+ );
+
+ default:
+ return null;
+ }
+ }
+};
+
+export default function Rule( props ) {
+ const {
+ canRemove,
+ field,
+ namePrefix,
+ operator,
+ value,
+ onChange,
+ onRemove,
+ } = props;
+
+ const fields = useSelect( select => select( 'audience' ).getFields(), [] );
+ const currentField = fields.find( fieldData => fieldData.name === field ) || {};
+
+ return (
+
+
+
+ onChange( { operator: e.target.value } ) }
+ type={ currentField.type || 'string' }
+ disabled={ fields.length === 0 }
+ />
+
+ onChange( { value: e.target.value } ) }
+ />
+
+ { canRemove && (
+
+ ) }
+
+ );
+}
diff --git a/src/audiences/edit/components/select-include.js b/src/audiences/edit/components/select-include.js
new file mode 100644
index 00000000..d6ec5364
--- /dev/null
+++ b/src/audiences/edit/components/select-include.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const { __ } = wp.i18n;
+
+const StyledSelect = styled.select`
+ vertical-align: middle;
+
+ &:not(:hover, :focus) {
+ border: none;
+ background: none;
+ margin-right: 8px;
+ }
+`;
+
+export default function SelectInclude( props ) {
+ return (
+
+
+
+
+ );
+}
+
+SelectInclude.defaultProps = {
+ label: '',
+};
diff --git a/src/audiences/edit/components/select-operator.js b/src/audiences/edit/components/select-operator.js
new file mode 100644
index 00000000..2aa9a3e0
--- /dev/null
+++ b/src/audiences/edit/components/select-operator.js
@@ -0,0 +1,26 @@
+import React from 'react';
+
+import {
+ NUMERIC_OPERATIONS,
+ STRING_OPERATIONS,
+} from '../data/constants';
+
+export default function SelectOperator( props ) {
+ const {
+ type = 'string',
+ } = props;
+
+ let options = STRING_OPERATIONS;
+
+ if ( type === 'number' ) {
+ options = NUMERIC_OPERATIONS;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/audiences/edit/data/constants.js b/src/audiences/edit/data/constants.js
new file mode 100644
index 00000000..68a138bc
--- /dev/null
+++ b/src/audiences/edit/data/constants.js
@@ -0,0 +1,16 @@
+const { __ } = wp.i18n;
+
+export const STRING_OPERATIONS = {
+ '=': __( 'is', 'altis-analytics' ),
+ '!=': __( 'is not', 'altis-analytics' ),
+ '*=': __( 'contains', 'altis-analytics' ),
+ '!*': __( 'does not contain', 'altis-analytics' ),
+ '^=': __( 'begins with', 'altis-analytics' ),
+};
+
+export const NUMERIC_OPERATIONS = {
+ 'gt': __( 'is greater than', 'altis-analytics' ),
+ 'gte': __( 'is greater than or equal to', 'altis-analytics' ),
+ 'lt': __( 'is less than', 'altis-analytics' ),
+ 'lte': __( 'is less than or equal to', 'altis-analytics' ),
+};
diff --git a/src/audiences/edit/data/defaults.js b/src/audiences/edit/data/defaults.js
new file mode 100644
index 00000000..b164b7ac
--- /dev/null
+++ b/src/audiences/edit/data/defaults.js
@@ -0,0 +1,26 @@
+export const defaultRule = {
+ field: '',
+ operator: '=', // =, !=, *, !*
+ value: '', // mixed
+ type: 'string', // data type, string or number
+};
+
+export const defaultGroup = {
+ include: 'any', // ANY, ALL, NONE
+ rules: [
+ defaultRule,
+ ],
+};
+
+export const defaultAudience = {
+ include: 'all', // ANY, ALL, NONE
+ groups: [
+ defaultGroup,
+ ],
+};
+
+export const defaultPost = {
+ title: { rendered: '' },
+ audience: defaultAudience,
+ status: 'draft',
+};
diff --git a/src/audiences/edit/data/index.js b/src/audiences/edit/data/index.js
new file mode 100644
index 00000000..8506bf18
--- /dev/null
+++ b/src/audiences/edit/data/index.js
@@ -0,0 +1,129 @@
+import {
+ defaultAudience,
+ defaultPost,
+} from './defaults';
+import reducer from './reducer';
+
+const { apiFetch } = wp;
+const { registerStore } = wp.data;
+
+const initialState = {
+ estimates: {},
+ fields: [],
+ pagination: {},
+ post: defaultPost,
+ posts: [],
+};
+
+// Hydrate from server side.
+if ( window.Altis.Analytics.Audiences.Fields ) {
+ initialState.fields = window.Altis.Analytics.Audiences.Fields;
+}
+if ( window.Altis.Analytics.Audiences.Current ) {
+ initialState.posts.push( window.Altis.Analytics.Audiences.Current );
+ initialState.post = window.Altis.Analytics.Audiences.Current;
+}
+
+const controls = {
+ FETCH_FROM_API( action ) {
+ return apiFetch( action.options );
+ },
+};
+
+const actions = {
+ setFields( fields ) {
+ return {
+ type: 'SET_FIELDS',
+ fields,
+ };
+ },
+ addPosts( posts ) {
+ return {
+ type: 'ADD_POSTS',
+ posts,
+ };
+ },
+ addEstimate( audience, estimate ) {
+ return {
+ type: 'ADD_ESTIMATE',
+ audience,
+ estimate,
+ };
+ },
+ setPost( post ) {
+ return {
+ type: 'SET_POST',
+ post,
+ };
+ },
+ fetch( options ) {
+ return {
+ type: 'FETCH_FROM_API',
+ options,
+ };
+ },
+};
+
+const selectors = {
+ getFields( state ) {
+ return state.fields;
+ },
+ getEstimate( state, audience ) {
+ const key = JSON.stringify( audience );
+ return state.estimates[ key ] || {
+ count: 0,
+ total: 0,
+ histogram: new Array( 28 ).fill( { count: 1 } ), // Build empty histrogram data.
+ };
+ },
+ getPost( state ) {
+ return state.post;
+ },
+ getPosts( state ) {
+ return state.posts;
+ },
+};
+
+const resolvers = {
+ *getFields() {
+ const fields = yield actions.fetch( {
+ path: 'analytics/v1/audiences/fields',
+ } );
+ return actions.setFields( fields );
+ },
+ *getEstimate( audience ) {
+ const audienceQuery = encodeURIComponent( JSON.stringify( audience ) );
+ const estimate = yield actions.fetch( {
+ path: `analytics/v1/audiences/estimate?audience=${ audienceQuery }`,
+ } );
+ return actions.addEstimate( audience, estimate );
+ },
+ *getPost( id ) {
+ const post = yield actions.fetch( {
+ path: `wp/v2/audiences/${ id }?context=edit`,
+ } );
+ if ( post.status === 'auto-draft' ) {
+ post.title.rendered = '';
+ }
+ if ( ! post.audience ) {
+ post.audience = defaultAudience;
+ }
+ yield actions.addPosts( [ post ] );
+ return actions.setPost( post );
+ },
+ *getPosts( page = 1, search = '' ) {
+ const posts = yield actions.fetch( {
+ path: `wp/v2/audiences?context=edit&page=${ page }&search=${ search }`,
+ } );
+ return actions.addPosts( posts );
+ },
+};
+
+export const store = registerStore( 'audience', {
+ actions,
+ controls,
+ initialState,
+ reducer,
+ resolvers,
+ selectors,
+} );
diff --git a/src/audiences/edit/data/reducer.js b/src/audiences/edit/data/reducer.js
new file mode 100644
index 00000000..474cdbbb
--- /dev/null
+++ b/src/audiences/edit/data/reducer.js
@@ -0,0 +1,69 @@
+import { unionBy } from 'lodash';
+
+export default function reducer( state, action ) {
+ switch ( action.type ) {
+ case 'SET_FIELDS': {
+ return {
+ ...state,
+ fields: action.fields,
+ };
+ }
+
+ case 'ADD_ESTIMATE': {
+ const key = JSON.stringify( action.audience );
+ if ( state.estimates[ key ] ) {
+ return state;
+ }
+ return {
+ ...state,
+ estimates: {
+ ...state.estimates,
+ [ key ]: action.estimate,
+ },
+ };
+ }
+
+ case 'ADD_POSTS': {
+ return {
+ ...state,
+ posts: unionBy( [ state.posts, action.posts ], post => post.id ),
+ };
+ }
+
+ case 'SET_POST': {
+ if ( ! action.post.id ) {
+ return {
+ ...state,
+ post: {
+ ...state.post,
+ ...action.post,
+ },
+ };
+ }
+
+ const posts = state.posts.map( post => {
+ if ( post.id !== action.post.id ) {
+ return post;
+ }
+
+ return {
+ ...post,
+ ...action.post,
+ };
+ } );
+
+ return {
+ ...state,
+ posts,
+ post: {
+ ...state.post,
+ ...action.post,
+ },
+ };
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/audiences/edit/index.js b/src/audiences/edit/index.js
new file mode 100644
index 00000000..84c51116
--- /dev/null
+++ b/src/audiences/edit/index.js
@@ -0,0 +1,232 @@
+import React, { Component } from 'react';
+import styled from 'styled-components';
+
+import AudienceEditor from './components/audience-editor';
+import Estimate from './components/estimate';
+import { defaultPost, defaultAudience } from './data/defaults';
+
+const { compose } = wp.compose;
+const {
+ withSelect,
+ withDispatch,
+} = wp.data;
+const { __ } = wp.i18n;
+const {
+ Button,
+ ToggleControl,
+ Notice,
+ Snackbar,
+} = wp.components;
+
+const formElement = document.querySelector( '.post-type-audience #post' );
+
+const StyledEdit = styled.div`
+ margin: 20px 0;
+
+ .components-notice {
+ margin: 0 0 20px;
+ }
+
+ .audience-settings {
+ display: flex;
+ }
+
+ .audience-settings > * {
+ flex: 1;
+ }
+
+ .audience-options {
+ flex: 0 1 320px;
+ margin: 20px 0 20px 40px;
+ }
+
+ .audience-estimate {
+ margin-bottom: 40px;
+ }
+`;
+
+class Edit extends Component {
+ state = {
+ notice: null,
+ error: null,
+ };
+
+ componentDidMount() {
+ // Get the form element if there is one. This is for back compat with
+ // the legacy post edit screen.
+ if ( formElement ) {
+ formElement.addEventListener( 'submit', this.onSubmit );
+ }
+ }
+
+ componentWillUnmount() {
+ if ( formElement ) {
+ formElement.removeEventListener( 'submit', this.onSubmit );
+ }
+ }
+
+ static getDerivedStateFromError( error ) {
+ return { error };
+ }
+
+ componentDidCatch( error, errorInfo ) {
+ console.error( error, errorInfo );
+ }
+
+ onSubmit = event => {
+ // Clear errors.
+ this.setState( { error: null } );
+
+ const { post } = this.props;
+
+ if ( post && post.title.rendered.length === 0 ) {
+ event.preventDefault();
+ this.setState( {
+ error: __( 'The title cannot be empty', 'altis-analytics' ),
+ } );
+ return;
+ }
+
+ // Prevent the "Leave site?" warning popup showing.
+ window.onbeforeunload = null;
+ window.jQuery && window.jQuery( window ).off( 'beforeunload' );
+ }
+
+ render() {
+ const {
+ loading,
+ post,
+ onSetTitle,
+ onSetAudience,
+ onSetStatus,
+ } = this.props;
+
+ const {
+ error,
+ notice,
+ } = this.state;
+
+ return (
+
+ { error && (
+ this.setState( { error: null } ) }
+ >
+ { error.toString() }
+
+ ) }
+ { notice && (
+ this.setState( { notice: null } ) }
+ >
+ { notice }
+
+ ) }
+
+
+ onSetTitle( e.target.value ) }
+ />
+
+
+
+
+
+
+
+
{ __( 'Audience options', 'altis-analytics' ) }
+ onSetStatus( post.status === 'publish' ? 'draft' : 'publish' ) }
+ />
+
+
+
+
+
+ );
+ }
+}
+
+Edit.defaultProps = {
+ postId: null,
+ post: defaultPost,
+ loading: false,
+ onCreate: () => { },
+ onSetTitle: () => { },
+ onSetAudience: () => { },
+ onSetStatus: () => { },
+};
+
+const applyWithSelect = withSelect( ( select, props ) => {
+ let post = props.post;
+
+ if ( props.postId ) {
+ post = select( 'audience' ).getPost( props.postId );
+ }
+
+ // If we have a post ID but no post then we're loading.
+ const loading = props.postId && ! post.id;
+
+ return {
+ post,
+ loading,
+ };
+} );
+
+const applyWithDispatch = withDispatch( dispatch => {
+ const store = dispatch( 'audience' );
+
+ return {
+ onSetTitle: value => {
+ store.setPost( { title: { rendered: value } } );
+ },
+ onSetAudience: value => {
+ store.setPost( { audience: value } );
+ },
+ onSetStatus: value => {
+ store.setPost( { status: value } );
+ },
+ };
+} );
+
+export default compose(
+ applyWithDispatch,
+ applyWithSelect,
+)( Edit );
diff --git a/src/audiences/index.css b/src/audiences/index.css
new file mode 100644
index 00000000..d4ffb753
--- /dev/null
+++ b/src/audiences/index.css
@@ -0,0 +1,15 @@
+/**
+ * Audiences admin listing styles.
+ */
+
+.post-type-audience .wp-list-table th#last_modified {
+ width: 10em;
+}
+
+.post-type-audience .wp-list-table .audience-estimate__percentage {
+ max-width: 60px;
+}
+
+.post-type-audience .wp-list-table .audience-estimate .audience-estimate__totals svg {
+ max-width: 120px;
+}
diff --git a/src/audiences/index.js b/src/audiences/index.js
new file mode 100644
index 00000000..e01df932
--- /dev/null
+++ b/src/audiences/index.js
@@ -0,0 +1,38 @@
+// Audience UI Application.
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import Edit from './edit';
+import Estimate from './edit/components/estimate';
+
+// Import the data store file directly.
+import './edit/data';
+
+// Get the audience UI placeholder.
+const AudienceUI = document.getElementById( 'altis-analytics-audience-ui' );
+
+// Is our audience UI placeholder present?
+if ( AudienceUI ) {
+ // Mount audience react app.
+ ReactDOM.render(
+ ,
+ AudienceUI
+ );
+}
+
+// Get the audience UI placeholder.
+const AudienceEstimates = document.querySelectorAll( '.altis-analytics-audience-estimate' );
+
+// Render any estimate blocks on page.
+AudienceEstimates.forEach( element => {
+ // Mount audience react app.
+ ReactDOM.render(
+ ,
+ element
+ );
+} );
diff --git a/webpack.config.js b/webpack.config.js
index 52fc0bc6..665e4af7 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,19 +1,27 @@
const path = require( 'path' );
+const webpack = require( 'webpack' );
const mode = process.env.NODE_ENV || 'production';
const BundleAnalyzerPlugin = require( 'webpack-bundle-analyzer' )
.BundleAnalyzerPlugin;
+const DynamicPublicPathPlugin = require( 'dynamic-public-path-webpack-plugin' );
+const SriPlugin = require( 'webpack-subresource-integrity' );
+const ManifestPlugin = require( 'webpack-manifest-plugin' );
+const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' );
const sharedConfig = {
mode: mode,
entry: {
analytics: path.resolve( __dirname, 'src/analytics.js' ),
+ audiences: path.resolve( __dirname, 'src/audiences/index.js' ),
},
output: {
path: path.resolve( __dirname, 'build' ),
- filename: '[name].js',
- publicPath: '.',
+ filename: '[name].[hash:8].js',
+ chunkFilename: 'chunk.[id].[chunkhash:8].js',
+ publicPath: '/',
libraryTarget: 'this',
- jsonpFunction: 'HManalyticsJSONP',
+ jsonpFunction: 'AltisAnalyticsJSONP',
+ crossOriginLoading: 'anonymous',
},
module: {
rules: [
@@ -29,6 +37,7 @@ const sharedConfig = {
],
plugins: [
require( '@babel/plugin-transform-runtime' ),
+ require( '@babel/plugin-proposal-class-properties' ),
require( '@wordpress/babel-plugin-import-jsx-pragma' ),
],
},
@@ -39,12 +48,15 @@ const sharedConfig = {
optimization: {
noEmitOnErrors: true,
},
- plugins: [],
- devtool: (
- mode === 'production'
- ? 'cheap-module-source-map'
- : 'cheap-module-eval-source-map'
- ),
+ plugins: [
+ new webpack.EnvironmentPlugin( {
+ SC_ATTR: 'data-styled-components-altis-analytics',
+ } ),
+ new ManifestPlugin( {
+ writeToFileEmit: true,
+ } ),
+ new CleanWebpackPlugin(),
+ ],
externals: {
'Altis': 'Altis',
'wp': 'wp',
@@ -53,6 +65,19 @@ const sharedConfig = {
},
};
+if ( mode === 'production' ) {
+ sharedConfig.plugins.push( new DynamicPublicPathPlugin( {
+ externalGlobal: 'window.Altis.Analytics.BuildURL',
+ chunkName: 'audiences',
+ } ) );
+ sharedConfig.plugins.push( new SriPlugin( {
+ hashFuncNames: [ 'sha384' ],
+ enabled: true,
+ } ) );
+} else {
+ sharedConfig.devtool = 'cheap-module-eval-source-map';
+}
+
if ( process.env.ANALYSE_BUNDLE ) {
// Add bundle analyser.
sharedConfig.plugins.push( new BundleAnalyzerPlugin() );