%PDF- %PDF-
Direktori : /var/www/cwg/wp-content/plugins/searchwp-metrics/assets/js/src/components/ |
Current File : //var/www/cwg/wp-content/plugins/searchwp-metrics/assets/js/src/components/Metrics.vue |
<template> <div class="searchwp-metrics wrap"> <div class="searchwp-metrics__title"> <h1>SearchWP Metrics</h1> <v-popover v-if="'1' === canEditSettings" :popover-wrapper-class="'searchwp-metrics-popover'" :placement="'bottom'" > <button class="button"><span class="dashicons dashicons-menu"></span></button> <template slot="popover"> <ul> <li> <button class="searchwp-metrics-nonbutton" v-close-popover @click.prevent="showingClearMetricsData = true" > {{ i18n.clearMetricsData }} </button> </li> <li v-if="ignoredQueries && ignoredQueries.length"> <button class="searchwp-metrics-nonbutton" v-close-popover @click.prevent="showingClearIgnoredQueries = true" > {{ i18n.removeAllIgnoredQueries }} </button> </li> <li> <button class="searchwp-metrics-nonbutton" v-close-popover @click.prevent="showingModifyLoggingRules = true" > {{ i18n.modifyLoggingRules }} </button> </li> <li> <button class="searchwp-metrics-nonbutton" v-close-popover @click.prevent="showingSettings = true" > {{ i18n.settings }} </button> </li> </ul> </template> </v-popover> <vue-modaltor :visible="showingClearMetricsData" @hide="hideClearMetricsData" :default-width="'300px'"> <div class="searchwp-metrics__modal searchwp-metrics__modal-confirmation"> <h4><span class="dashicons dashicons-arrow-right"></span> {{ i18n.areYouSure }} <span class="dashicons dashicons-arrow-left"></span></h4> <component :is="translatedClearMetricsDataNote"></component> <ul class="searchwp-metrics__modal-confirmation--actions"> <li> <button @click.prevent="clearMetricsData" class="button"> {{ i18n.clearData }} </button> </li> <li> <button @click.prevent="hideClearMetricsData" class="searchwp-metrics-nonbutton"> {{ i18n.cancel }} </button> </li> </ul> </div> </vue-modaltor> <vue-modaltor :visible="showingClearIgnoredQueries" @hide="hideClearIgnoredQueries" :default-width="'300px'"> <div class="searchwp-metrics__modal searchwp-metrics__modal-confirmation"> <h4><span class="dashicons dashicons-arrow-right"></span> {{ i18n.areYouSure }} <span class="dashicons dashicons-arrow-left"></span></h4> <component :is="translatedClearIgnoredQueriesNote"></component> <ul class="searchwp-metrics__modal-confirmation--actions"> <li> <button @click.prevent="clearIgnoredQueries" class="button"> {{ i18n.clearData }} </button> </li> <li> <button @click.prevent="hideClearIgnoredQueries" class="searchwp-metrics-nonbutton"> {{ i18n.cancel }} </button> </li> </ul> </div> </vue-modaltor> <vue-modaltor :visible="showingModifyLoggingRules" @hide="hideModifyLoggingRules" :default-width="'400px'"> <div class="searchwp-metrics__modal searchwp-metrics__modal-logging"> <h4>{{ i18n.loggingRules }}</h4> <p>{{ i18n.loggingRulesNote }}</p> <p><span class="dashicons dashicons-info"></span> <component :is="translatedLoggingRulesNoteDetails"></component></p> <div class="searchwp-metrics__textarea"> <label for="searchwp_metrics_ip_blocklist">{{ i18n.userIdRoleBlocklist }}</label> <textarea v-model="ignoredRoles" name="searchwp_metrics_role_blocklist" id="searchwp_metrics_role_blocklist" cols="30" rows="10"></textarea> <p class="description">{{ i18n.userIdRoleBlocklistNote }}</p> </div> <div class="searchwp-metrics__textarea"> <label for="searchwp_metrics_ip_blocklist">{{ i18n.ipBlocklist }}</label> <textarea v-model="ignoredIps" name="searchwp_metrics_ip_blocklist" id="searchwp_metrics_ip_blocklist" cols="30" rows="10"></textarea> <p class="description">{{ i18n.ipBlocklistNote }}</p> </div> <ul class="searchwp-metrics__modal-confirmation--actions"> <li> <button @click.prevent="hideModifyLoggingRules" class="button"> {{ i18n.saveClose }} </button> </li> </ul> </div> </vue-modaltor> <vue-modaltor :visible="showingSettings" @hide="hideSettings" :default-width="'400px'"> <div class="searchwp-metrics__modal searchwp-metrics__modal-settings"> <h4>{{ i18n.settings }}</h4> <div v-if="clickTrackingBuoyApplicable" class="searchwp-metrics__checkbox"> <input v-model="clickTrackingBuoy" type="checkbox" name="searchwp_metrics_click_track_buoy" id="searchwp_metrics_click_track_buoy" /> <div class="searchwp-metrics__checkbox-label"> <label for="searchwp_metrics_click_track_buoy">{{ i18n.clickTrackingBuoy }}</label> <p class="description">{{ i18n.clickTrackingBuoyLabelNote }}</p> </div> </div> <div v-else class="searchwp-metrics__note"> <span class="dashicons dashicons-info"></span> <div> <p>{{ i18n.clickTrackingBuoyUnavailable }}</p> </div> </div> <div class="searchwp-metrics__checkbox"> <input v-model="clearDataOnUninstall" type="checkbox" name="searchwp_metrics_clear_data_on_uninstall" id="searchwp_metrics_clear_data_on_uninstall" /> <div class="searchwp-metrics__checkbox-label"> <label for="searchwp_metrics_clear_data_on_uninstall">{{ i18n.removeOnUninstallation }}</label> <p class="description">{{ i18n.removeOnUninstallationLabelNote }}</p> </div> </div> <ul class="searchwp-metrics__modal-confirmation--actions"> <li> <button @click.prevent="hideSettings" class="button"> {{ i18n.saveClose }} </button> </li> </ul> </div> </vue-modaltor> </div> <div class="searchwp-metrics__controls"> <div class="searchwp-metrics__control"> <h4>{{ i18n.dateRange }}</h4> <vue-datepicker-local v-model="dateRange" :range-separator="i18n.to" :local="i18n.datePicker" show-buttons @confirm="update" ></vue-datepicker-local> </div> <div class="searchwp-metrics__control"> <h4>{{ i18n.searchQueryControls }}</h4> <div class="searchwp-metrics__control-queries"> <multiselect v-model="selectedSearchQueries" id="searchwp-metrics-query-limiter" label="query" track-by="id" :placeholder="i18n.limitMetricsToQueries" open-direction="bottom" :options="searchQueries" :multiple="true" :searchable="true" :loading="isLoadingSearchSearches" :internal-search="false" :clear-on-select="false" :close-on-select="false" :options-limit="300" :limit="3" :max-height="300" :show-no-results="false" :hide-selected="true" @search-change="searchSearchQueries" :taggable="true" :tag-placeholder="i18n.addAsPartialMatch" @tag="addSelectedSearchQuery" @input="update" ></multiselect> <button class="button" @click="showingIgnoredSearches = true">{{ i18n.ignored }}: {{ Object.keys(ignoredQueries).length }}</button> <vue-modaltor :visible="showingIgnoredSearches" @hide="hideIgnoredSearches" :default-width="'600px'"> <div class="searchwp-metrics__modal searchwp-metrics__ignored-searches-details"> <h4>{{ i18n.ignoredSearches }}</h4> <p>{{ i18n.ignoredMessage }}</p> <p><button class="button" @click="addNewIgnoredSearch">Add</button></p> <table v-if="ignoredQueries && ignoredQueries.length"> <thead> <tr> <th>{{ i18n.ignoredSearchQuery }}</th> </tr> </thead> <tbody> <tr v-for="(ignoredQuery, ignoredQueryIndex) in ignoredQueries" :key="'ignored' + ignoredQueryIndex" v-if="!ignoredQuery.unignored"> <td> <delete :title="i18n.stopIgnoringQuery" v-on:onclick="unIgnoreQuery(ignoredQuery.hash)"></delete> {{ ignoredQuery.query }} </td> </tr> </tbody> </table> <div v-else class="searchwp-metrics__note"> <span class="dashicons dashicons-info"></span> <div> <p>{{ i18n.noIgnoredQueries }}</p> </div> </div> </div> </vue-modaltor> </div> </div> <div class="searchwp-metrics__control"> <h4>{{ i18n.enginesToDisplay }}</h4> <multiselect v-model="multiselect.engines.value" :options="multiselect.engines.options" :multiple="true" :close-on-select="true" :hide-selected="true" :placeholder="i18n.chooseEngine" label="label" track-by="name" :searchable="false" :allow-empty="false" @input="update" ></multiselect> </div> </div> <div class="searchwp-metrics__details"> <div v-if="!sameDate" v-bind:class="['searchwp-metrics__searches-over-time', searchesOverTime ? '' : 'searchwp-metrics__is-loading']"> <line-chart :datacollection="searchesOverTime" :options="searchesOverTimeOptions" :height="'300px'" ></line-chart> </div> <div class="searchwp-metrics__engine-details" v-if="engines && !loading" v-for="(engine, engineIndex) in engines" :key="engine.name"> <div class="searchwp-metrics__engine-details-heading-group"> <h3 class="searchwp-metrics__engine-details-heading"> <span class="searchwp-metrics__engine-details-legend" v-bind:style="{ backgroundColor: engine.color }"></span> <component :is="translatedEngineDetailsHeading" :props="engine"></component> </h3> <v-popover :popover-wrapper-class="'searchwp-metrics-popover'" :placement="'left'" > <button class="button"><span class="dashicons dashicons-menu"></span></button> <template slot="popover"> <ul> <li> <json-csv :data = "formattedSearchesOverTimeForCsv(engineIndex)" :fields = "searchesOverTimeJsonFields" type = "csv" :name = "appendTimestamp('SearchesOverTime_' + engine.name) + '.csv'"> {{ i18n.exportSearchesOverTime }} </json-csv> </li> <li> <json-csv :data = "formattedEngineStatisticForCsv(engineIndex, engine)" :fields = "engineStatisticsJsonFields" type = "csv" :name = "appendTimestamp('EngineStatistics_' + engine.name) + '.csv'"> {{ i18n.exportEngineStatistics }} </json-csv> </li> <li> <json-csv :data = "formattedPopularSearchesForCsv(engineIndex)" :fields = "popularSearchesJsonFields" type = "csv" :name = "appendTimestamp('PopularSearches') + '.csv'"> {{ i18n.exportPopularSearches }} </json-csv> </li> </ul> </template> </v-popover> </div> <div v-if="0 == getTotalSearchesCount( engineIndex )" class="searchwp-metrics__no-data"> <p>{{ i18n.notEnoughData }}</p> </div> <div v-else class="searchwp-metrics__engine-details-hook"> <div class="searchwp-metrics__engine-details-alpha"> <div class="searchwp-metrics__engine-details--heading"> <h4>{{ i18n.engineStatistics }}</h4> </div> <div class="searchwp-metrics__stats-grid"> <div> <dl> <dt>{{ i18n.totalSearches }}</dt> <dd>{{ getTotalSearchesCount( engineIndex ) }}</dd> </dl> </div> <div> <dl> <dt>{{ i18n.noResultsSearches }}</dt> <dd> <div class="searchwp-metrics__flex"> <div>{{ getFailedSearchesCount( engineIndex ) }}</div> <div> <button @click="showingFailedSearches = engine.name" class="searchwp-trigger__external"> <span class="dashicons dashicons-external"></span> <span class="screen-reader-text">{{ i18n.viewNoResultsSearches }}</span> </button> </div> </div> </dd> </dl> <vue-modaltor :visible="showingFailedSearches === engine.name" @hide="hideFailedSearches" :default-width="'600px'"> <div class="searchwp-metrics__modal searchwp-metrics__failed-searches-details"> <component :is="translatedNoResultsSearchesEngineHeading" :props="engine"></component> <div class="searchwp-metrics__engine-details--heading" v-if="getFailedSearches(engine.name) && getFailedSearches(engine.name).length" > <component :is="translatedNoResultsSearchesEngineNote"></component> <json-csv class="button" :data = "noResultsDetailsForExport" :fields = "noResultsDetailsJsonFields" type = "csv" :name = "appendTimestamp('NoResultsSearches') + '.csv'"> <span class="dashicons dashicons-download"></span> </json-csv> </div> <table v-if="getFailedSearches(engine.name) && getFailedSearches(engine.name).length"> <thead> <tr> <th>{{ i18n.searchQuery }}</th> <th>{{ i18n.searches }}</th> </tr> </thead> <tbody> <tr v-for="(failedSearch, failedSearchIndex) in getFailedSearches(engine.name)" :key="'failed' + failedSearchIndex + engine.name" v-bind:id="'searchwp-metrics--hook-failed-' + failedSearchIndex"> <td> <delete title="Ignore this query" v-on:onclick="ignoreFailedSearch(failedSearch.query, engine.name, failedSearchIndex)"></delete> {{ failedSearch.query }} </td> <td>{{ failedSearch.count }}</td> </tr> </tbody> </table> <div v-else class="searchwp-metrics__note"> <span class="dashicons dashicons-info"></span> <div> <p>{{ i18n.noFailedSearches }}</p> </div> </div> </div> </vue-modaltor> </div> <div> <dl> <dt>{{ i18n.totalResultsViewed }}</dt> <dd>{{ outputMetric( totalClicks[ engine.name ].statistic ) }}</dd> </dl> </div> <div> <dl> <dt> <tooltip :content="i18n.searchesPerUserNote">{{ i18n.searchesPerUser }}</tooltip> </dt> <dd>{{ outputMetric( averageSearchesPerUser[ engine.name ].statistic ) }}</dd> </dl> </div> <div> <dl> <dt>{{ i18n.clicksPerSearch }}</dt> <dd>{{ outputMetric( averageClicksPerSearch[ engine.name ].statistic ) }}</dd> </dl> </div> <div> <dl> <dt>{{ i18n.averageClickRank }}</dt> <dd>{{ outputMetric( averageClickRank[ engine.name ].statistic ) }}</dd> </dl> </div> </div> <div class="searchwp-metrics__note" v-if="engineHasNoTracking( engine.name )"> <span class="dashicons dashicons-info"></span> <div> <component :is="translatedClickTrackingNote"></component> </div> </div> </div> <div class="searchwp-metrics__engine-details-beta searchwp-metrics__engine-popular-searches"> <div class="searchwp-metrics__engine-details--heading"> <h4>{{ i18n.popularSearches }}</h4> <div> <button class="button" @click.prevent="showPopularSearchDetails(engine)">{{ i18n.viewMore }}</button> </div> </div> <div v-if="getPopularSearches(engine.name)" class="searchwp-metrics__chart-donut-wrapper"> <div> <table class="searchwp-metrics-table"> <tbody> <tr v-for="(popularSearch, popularSearchIndex) in getPopularSearches(engine.name).labels" :key="'popularSearch' + engine.name + popularSearchIndex"> <td> <legend-indicator :index="popularSearchIndex" :text="popularSearch" v-on:onclick="showPopularSearchDetails(engine, popularSearch)" ></legend-indicator> </td> <td><delete v-on:onclick="ignoreSearch(popularSearch)"></delete></td> <td>{{ getPopularSearches(engine.name).datasets[0].data[ popularSearchIndex ] }}</td> </tr> </tbody> </table> <vue-modaltor :visible="showingPopularSearchDetails && showingPopularSearchDetails.engine.name === engine.name" @hide="hidePopularSearchDetails" :default-width="'800px'"> <div v-bind:class="['searchwp-metrics__modal searchwp-metrics__popular-search-details', loadingPopularSearchDetails ? 'searchwp-metrics__is-loading-details' : '']" v-if="showingPopularSearchDetails" > <div class="searchwp-metrics__loading-details-container"> <component :is="translatedPopularSearchDetailsHeading" :props="engine"></component> <div class="searchwp-metrics__engine-details--heading"> <p>{{ i18n.popularSearchDetailsNote }}</p> <div> <input type="text" v-model.number="popularSearchesCount" @keyup.enter="updatePopularSearchDetails(showingPopularSearchDetails.engine.name)"> <button class="button" @click.prevent="updatePopularSearchDetails(showingPopularSearchDetails.engine.name)"> <span class="dashicons dashicons-update"></span> </button> <json-csv class="button" :data = "popularSearchesDetailsForExport" :fields = "popularSearchesDetailsJsonFields" type = "csv" :name = "appendTimestamp('PopularSearchesDetails') + '.csv'"> <span class="dashicons dashicons-download"></span> </json-csv> </div> </div> <v-collapse-group> <div class="searchwp-metrics__split"> <p class="searchwp-metrics__guide">{{ i18n.searchQuery }}</p> <p class="searchwp-metrics__guide">{{ i18n.searches }}</p> </div> <div class="searchwp-metrics__popular-search-details searchwp-metrics__accordion"> <v-collapse-wrapper v-for="(popularSearchDetail, popularSearchDetailIndex) in popularSearchesDetails" v-bind:active="popularSearchDetail.query.query === showingPopularSearchDetails.query" v-bind:id="'searchwp-metrics--hook-popular-' + popularSearchDetailIndex" :key="'popularSearchDetail' + showingPopularSearchDetails.engine.name + popularSearchDetailIndex"> <div class="searchwp-metrics__accordion--header"> <div class="searchwp-metrics__accordion--trigger" v-collapse-toggle> <div class="searchwp-metrics__accordion--header-icon"> <span class="dashicons dashicons-arrow-right"></span> </div> <h5 class="searchwp-metrics__accordion--header-title"> {{ popularSearchDetail.query.query }} </h5> <div class="searchwp-metrics__accordion--header-figure"> {{ popularSearchDetail.query.searchcount }} </div> </div> <div class="searchwp-metrics__accordion--actions"> <delete v-on:onclick="ignorePopularSearch(popularSearchDetail.query.query, popularSearchDetailIndex)"></delete> </div> </div> <div class="searchwp-metrics__accordion--content" v-collapse-content> <div class="searchwp-metrics__inner"> <div v-if="popularSearchDetail.clicks.length"> <bar-chart :datacollection="buildChartDataset(popularSearchDetail.clicks)" :options="popularSearchDetailsClicksOptions" :height="'30px'" ></bar-chart> <table> <thead> <tr> <th class="searchwp-metrics--primary-col">{{ i18n.clickedResult }}</th> <th class="searchwp-metrics--secondary-col">{{ i18n.clicks }}</th> <th class="searchwp-metrics--secondary-col">{{ i18n.conversionRate }}</th> </tr> </thead> <tbody> <tr v-for="(popularSearchDetailClicks, popularSearchDetailClicksIndex) in popularSearchDetail.clicks" :key="'popularSearchDetailClicks' + showingPopularSearchDetails.engine.name + popularSearchDetailClicksIndex"> <td class="searchwp-metrics--primary-col"> <a :href="popularSearchDetailClicks.permalink" target="_blank"> <legend-indicator :index="popularSearchDetailClicksIndex" :text="popularSearchDetailClicks.post_title" ></legend-indicator> </a> </td> <td class="searchwp-metrics--secondary-col">{{ popularSearchDetailClicks.clicks }}</td> <td class="searchwp-metrics--secondary-col">{{ (( popularSearchDetailClicks.clicks * 100 ) / popularSearchDetail.query.searchcount).toFixed(2) }}%</td> </tr> </tbody> </table> </div> <div v-else class="searchwp-metrics__no-data"> <p>{{ i18n.noClicks }}</p> </div> </div> </div> </v-collapse-wrapper> </div> </v-collapse-group> </div> <div class="searchwp-metrics__loading-details-loader"> <spinner :size="55" :line-size="6" ></spinner> </div> </div> </vue-modaltor> </div> <div> <doughnut-chart :datacollection="getPopularSearches(engine.name)" :options="popularSearchesOptions" ></doughnut-chart> <div class="searchwp-metrics__engine-popular-searches-coverage"> <span>{{ getPopularSearchesPercentage(engine.name, engineIndex) }}%</span> {{ i18n.ofAllSearches }} </div> </div> </div> </div> <div class="searchwp-metrics__engine-details-omega searchwp-metrics__engine-suggestions"> <div v-if="getPopularClicks(engine.name)"> <div class="searchwp-metrics__engine-details--heading"> <h4>{{ i18n.insights }}</h4> </div> <div class="searchwp-metrics__engine-suggestions-insights"> <div v-if="getInsights(engine.name) && getInsights(engine.name).length"> <ul> <li class="searchwp-metrics__engine-suggestions-insight" v-for="(insight, insightIndex) in getInsights(engine.name)" :key="'insight' + engine.name + insightIndex"> <insight-underdog v-if="insight.type === 'underdog'" :post-count="insight.postCount" v-on:onclick="showInsightDetails(engine, insightIndex)" ></insight-underdog> <insight-popular v-if="insight.type === 'popular'" :post-count="insight.postCount" v-on:onclick="showInsightDetails(engine, insightIndex)" ></insight-popular> <insight-analysis v-if="insight.type === 'analysis'" :query="insight.query" :click-count="insight.clickCount" :post-count="insight.postCount" v-on:onclick="showInsightDetails(engine, insightIndex)" ></insight-analysis> </li> </ul> <div class="searchwp-metrics__engine-suggestions-insights-actions" v-if="getInsightsCount(engine.name) > 5" > <button class="button" @click.prevent="showInsightDetails(engine)">{{ i18n.viewAll }} ({{ getInsightsCount(engine.name) }})</button> </div> </div> <div class="searchwp-metrics__note" v-else> <span class="dashicons dashicons-info"></span> <div> <p>{{ i18n.noInsights }}</p> </div> </div> </div> <vue-modaltor :visible="showingInsights && showingInsights.engine.name === engine.name" @hide="hideInsights" :default-width="'600px'"> <div v-if="showingInsights && showingInsights.engine.name === engine.name" class="searchwp-metrics__modal searchwp-metrics__engine-suggestions-insights-details"> <component :is="translatedInsightsEngineHeading" :props="engine"></component> <v-collapse-group> <div class="searchwp-metrics__accordion"> <v-collapse-wrapper v-for="(insightDetail, insightDetailIndex) in getInsights(engine.name, -1)" v-bind:active="insightDetailIndex === showingInsights.insightIndex" :key="'insightDetail' + engine.name + insightDetailIndex"> <div class="searchwp-metrics__accordion--header"> <div class="searchwp-metrics__accordion--trigger" v-collapse-toggle> <div class="searchwp-metrics__accordion--header-icon"> <span v-if="insightDetail.type === 'underdog'" class="dashicons dashicons-sos"></span> <span v-if="insightDetail.type === 'popular'" class="dashicons dashicons-awards"></span> <span v-if="insightDetail.type === 'analysis'" class="dashicons dashicons-arrow-right"></span> </div> <div class="searchwp-metrics__accordion--header-title"> <insight-underdog v-if="insightDetail.type === 'underdog'" :post-count="insightDetail.postCount" :icon="''" ></insight-underdog> <insight-popular v-if="insightDetail.type === 'popular'" :post-count="insightDetail.postCount" :icon="''" ></insight-popular> <insight-analysis v-if="insightDetail.type === 'analysis'" :query="insightDetail.query" :click-count="insightDetail.clickCount" :post-count="insightDetail.postCount" :icon="''" ></insight-analysis> </div> </div> </div> <div class="searchwp-metrics__accordion--content" v-collapse-content> <div class="searchwp-metrics__inner"> <div v-if="insightDetail.type === 'underdog'"> <table> <thead> <tr> <th class="searchwp-metrics--primary-col">{{ i18n.entry }}</th> <th class="searchwp-metrics--secondary-col">{{ i18n.clicks }}</th> <th class="searchwp-metrics--secondary-col">{{ i18n.averageRank }}</th> </tr> </thead> <tbody> <tr v-for="(insightPost, insightPostIndex) in insightDetail.posts" :key="'insightPost' + engine.name + insightPostIndex" > <td class="searchwp-metrics--primary-col"> <a :href="insightPost.permalink" target="_blank"> {{ insightPost.post_title }} </a> </td> <td class="searchwp-metrics--secondary-col">{{ insightPost.click_count }}</td> <td class="searchwp-metrics--secondary-col">{{ insightPost.avg_rank }}</td> </tr> </tbody> </table> </div> <div v-if="insightDetail.type === 'popular'"> <table> <thead> <tr> <th class="searchwp-metrics--primary-col">{{ i18n.entry }}</th> <th class="searchwp-metrics--secondary-col">{{ i18n.clicks }}</th> </tr> </thead> <tbody> <tr v-for="(insightPost, insightPostIndex) in insightDetail.posts" :key="'insightPost' + engine.name + insightPostIndex" > <td class="searchwp-metrics--primary-col"> <a :href="insightPost.permalink" target="_blank"> {{ insightPost.post_title }} </a> </td> <td class="searchwp-metrics--secondary-col">{{ insightPost.clicks }}</td> </tr> </tbody> </table> </div> <div v-if="insightDetail.type === 'analysis'"> <bar-chart :datacollection="buildChartDataset(insightDetail.posts, insightDetail.clickCount)" :options="popularSearchDetailsClicksOptions" :height="'30px'" ></bar-chart> <table> <thead> <tr> <th class="searchwp-metrics--primary-col">{{ i18n.entry }}</th> <th class="searchwp-metrics--secondary-col">{{ i18n.clicks }}</th> </tr> </thead> <tbody> <tr v-for="(insightDetailInfo, insightDetailIndex) in insightDetail.posts" :key="'insightDetail' + engine.name + insightDetailIndex"> <td class="searchwp-metrics--primary-col"> <a :href="insightDetailInfo.permalink" target="_blank"> <legend-indicator :index="insightDetailIndex" :text="insightDetailInfo.post_title" ></legend-indicator> </a> </td> <td class="searchwp-metrics--secondary-col">{{ insightDetailInfo.clicks }}</td> </tr> </tbody> </table> </div> </div> </div> </v-collapse-wrapper> </div> </v-collapse-group> </div> </vue-modaltor> </div> </div> </div> </div> </div> <transition name="fade"> <div class="searchwp-metrics__loading" v-if="loading" v-bind:style="{ top: loaderPositionTop + 'px', left: loaderPositionLeft + 'px' }"> <spinner :size="55" :line-size="6" ></spinner> </div> </transition> </div> </template> <script> import '../../../../node_modules/vue-multiselect/dist/vue-multiselect.min.css'; import Vue from 'vue'; import Debounce from "debounce"; import Keyfinder from "keyfinder"; import Multiselect from 'vue-multiselect'; import VueDatepickerLocal from 'vue-datepicker-local'; import Tooltip from './Tooltip.vue'; import LineChart from './LineChart.vue'; import Delete from './Delete.vue'; import DoughnutChart from './DoughnutChart.vue'; import BarChart from './BarChart.vue'; import LegendIndicator from './LegendIndicator.vue'; import InsightAnalysis from './InsightAnalysis.vue'; import InsightPopular from './InsightPopular.vue'; import InsightUnderdog from './InsightUnderdog.vue'; import Spinner from 'vue-simple-spinner'; import JsonCsv from 'vue-json-excel'; export default { name: 'SearchwpMetrics', components: { Multiselect, VueDatepickerLocal, LineChart, Tooltip, DoughnutChart, BarChart, Delete, LegendIndicator, InsightAnalysis, InsightPopular, InsightUnderdog, Spinner, JsonCsv }, methods: { addNewIgnoredSearch: function() { let newIgnoredSearch = prompt('Enter search to ignore'); newIgnoredSearch = newIgnoredSearch.trim(); if(newIgnoredSearch){ this.showingIgnoredSearches = false; this.ignoreSearch(newIgnoredSearch,true); } }, clearMetricsData: function() { let self = this; let payload = { action: 'searchwp_metrics_clear_metrics_data', }; // We want the loading state to be triggered right away this.loading = true; this.hideClearMetricsData(); // Request the deletion and then update when it's done self.apiRequest(payload).then((response) => { this.update(); }); }, hideClearMetricsData: function(){ this.showingClearMetricsData = false; }, clearIgnoredQueries: function() { let self = this; let payload = { action: 'searchwp_metrics_clear_ignored_queries', }; // We want the loading state to be triggered right away this.loading = true; this.hideClearIgnoredQueries(); // Request the deletion and then update when it's done self.apiRequest(payload).then((response) => { this.update(); }); }, hideClearIgnoredQueries: function() { this.showingClearIgnoredQueries = false; }, hideModifyLoggingRules: function() { this.showingModifyLoggingRules = false; let self = this; let payload = { action: 'searchwp_metrics_update_logging_rules', ips: this.ignoredIps, roles: this.ignoredRoles }; self.apiRequest(payload).then((response) => {}); }, hideSettings: function() { this.showingSettings = false; let self = this; let payload = { action: 'searchwp_metrics_update_settings', clear_data_on_uninstall: this.clearDataOnUninstall, click_tracking_buoy: this.clickTrackingBuoy }; self.apiRequest(payload).then((response) => {}); }, formattedPopularSearchesForCsv: function(engineIndex){ if (!this.popularSearches || !this.popularSearches[ engineIndex ]) { return []; } let data = this.popularSearches[ engineIndex ].datasets[0].data; let labels = this.popularSearches[ engineIndex ].labels; let formatted = []; for (let i = 0; i < labels.length; i++) { // The output will be nested in a query object so as to utilize the existing Popular Search Details format formatted.push({ query: { query: labels[i], searchcount: data[i], } }); } return formatted; }, formattedSearchesOverTimeForCsv: function(engineIndex){ if (!this.searchesOverTime || !this.searchesOverTime.datasets[ engineIndex ]) { return []; } let data = this.searchesOverTime.datasets[ engineIndex ].data; let labels = this.searchesOverTime.labels; let formatted = []; for (let i = 0; i < labels.length; i++) { formatted.push({ label: labels[i], searches: data[i], }); } return formatted; }, formattedEngineStatisticForCsv: function(engineIndex, engine = 'default') { return [ { statistic: 'Total Searches', value: this.getTotalSearchesCount( engineIndex ) }, { statistic: 'No Results Searches', value: this.getFailedSearchesCount( engineIndex ) }, { statistic: 'Total Results Viewed', value: this.outputMetric( this.totalClicks[ engine.name ].statistic ) }, { statistic: 'Searches Per User', value: this.outputMetric( this.averageSearchesPerUser[ engine.name ].statistic ) }, { statistic: 'Clicks Per Search', value: this.outputMetric( this.averageClicksPerSearch[ engine.name ].statistic ) }, { statistic: 'Average Click Rank', value: this.outputMetric( this.averageClickRank[ engine.name ].statistic ) } ]; }, appendTimestamp: function(string = 'export'){ return string + '_' + this.formatDate(this.dateRange[0]) + '_' + this.formatDate(this.dateRange[1]); }, // TODO: the params here are terrible showPopularSearchDetails: function(engine = 'default', query = ''){ this.showingPopularSearchDetails = { engine: engine, query: query }; this.updatePopularSearchDetails(engine.name); }, // TODO: the params here are terrible showInsightDetails: function(engine = 'default', insightIndex = -1){ this.showingInsights = { engine: engine, insightIndex: insightIndex }; }, buildChartDataset: function(data, total = 0){ let labels = ['Clicks']; let datasets = []; // This is kind of wacky because it's being used for Popular Searches and Insights and the data // structure is a bit different; Analysis Insights already know the total number of clicks // so in those cases that param is populated, otherwise this figures it out let totalClicks = total === 0 ? Keyfinder(data,'clicks').reduce((a, b) => a + b, 0) : total; for (let i = 0; i < data.length; i++) { let value = (( data[i].clicks * 100 ) / totalClicks).toFixed(2) datasets.push({ type: 'horizontalBar', backgroundColor: Vue.SearchwpMetricsGetColor(i), label: data[i].post_title, data: [ value ] }); } return { labels: labels, datasets: datasets } }, updatePopularSearchDetails: function(engine) { let self = this; self.loadingPopularSearchDetails = true; let payload = { action: 'searchwp_metrics_popular_search_details', engine: engine, limit: this.popularSearchesCount, }; self.apiRequest(payload).then((response) => { // We need to 'reset' the rules of any inline ignores if (this.inlineIgnores.length) { this.inlineIgnores.forEach(function(index){ document.getElementById('searchwp-metrics--hook-popular-' + index).removeAttribute('style'); }); // Empty out the array this.inlineIgnores = []; } self.popularSearchesDetails = response.data; self.loadingPopularSearchDetails = false; }); }, unIgnoreQuery: function(hash) { // Remove from display (we're not refreshing until modal is closed) let newIgnoredQueries = this.ignoredQueries; for (let i = 0; i < this.ignoredQueries.length; i++) { if (hash === this.ignoredQueries[i].hash) { Vue.set(this.ignoredQueries[i], 'unignored', true); break; } } this.unIgnoreSearch(hash, false); this.needsRefresh = true; }, unIgnoreSearch: function(hash, refresh = true) { let self = this; let payload = { action: 'searchwp_metrics_unignore_query', hash: hash }; self.apiRequest(payload).then((response) => { if (refresh) { self.update(); } }); }, hideInsights: function(){ this.showingInsights = false; }, searchSearchQueries: Debounce(function(query) { let self = this; if (query.length<3) { return; } self.isLoadingSearchSearches = true; let payload = { action: 'searchwp_metrics_search_queries', searchquery: query }; self.apiRequest(payload).then((response) => { self.searchQueries = response.data; self.isLoadingSearchSearches = false; }); }, 500), addSelectedSearchQuery: function(query){ // In this case we've added a partial match so we're going to use the query as the ID // and the server will consider it for a partial match let tag = { id: query, query: query }; this.searchQueries.push(tag); this.selectedSearchQueries.push(tag); this.update(); }, limit: function(data, limit = 10) { // As it stands this causes a Vue Warning about a potential infinite render loop, but // I'm not sure why yet, so we're going to hide the data with CSS while other tests are run let limited; let i = 0; // Is it an object? if (data.length === undefined) { limited = {}; for (let property in data) { if (data.hasOwnProperty(property)) { i++; limited[property] = data[property]; if (Object.keys(limited).length >= limit) { break; } } } } else { limited = data.splice(0,limit); } return limited; }, engineHasNoTracking: function(engine) { return this.outputMetric( this.totalClicks[ engine ].statistic ) === '--' || this.outputMetric( this.averageClicksPerSearch[ engine ].statistic ) === '--' || this.outputMetric( this.averageClickRank[ engine ].statistic ) === '--'; }, outputMetric: function(n) { if ('0' === n.toString() || '0.000' === n.toString()) { return '--'; } else { return n; } }, hidePopularSearchDetails: function() { this.showingPopularSearchDetails = false; // We only need to update stats on close if new ignored searches were added if (this.needsRefresh) { this.update(); this.needsRefresh = false; } }, hideFailedSearches: function() { this.showingFailedSearches = false; // We need to 'reset' the rules of any inline ignores if (this.inlineIgnores.length) { let self = this; // Persist the ignore in the data model self.inlineIgnores.forEach(function(ignore){ for (let i = 0; i < self.failedSearches.length; i++) { if (self.failedSearches[i].engine === ignore.engine) { for (let y = 0; y < self.failedSearches[i].data.length; y++) { if (ignore.ignoredSearch === self.failedSearches[i].data[y].query) { Vue.delete(self.failedSearches[i].data, y); break; } } break; } } }); // Remove the inline style in case of reload this.inlineIgnores.forEach(function(ignore){ document.getElementById('searchwp-metrics--hook-failed-' + ignore.index).removeAttribute('style'); }); // Empty out the array this.inlineIgnores = []; } // We only need to update stats on close if new ignored searches were added if (this.needsRefresh) { this.update(); this.needsRefresh = false; } }, hideIgnoredSearches: function() { this.showingIgnoredSearches = false; // We only need to update stats on close if ignored searches were removed if (this.needsRefresh) { this.update(); this.needsRefresh = false; } }, getTotalSearchesCount: function( engineIndex ) { if (!this.searchesOverTime || !this.searchesOverTime.datasets[ engineIndex ]) { return 0; } let data = this.searchesOverTime.datasets[ engineIndex ].data; return data.reduce((a, b) => a + b, 0); }, getFailedSearchesCount: function( engineIndex ) { if (!this.failedSearches || !this.failedSearches[ engineIndex ]) { return 0; } let data = this.failedSearches[ engineIndex ].data; return data.reduce((a, b) => a + b.count, 0); }, ignoreSearch: function(ignoredSearch, refresh = true) { let self = this; let payload = { action: 'searchwp_metrics_ignore_query', query: ignoredSearch }; self.apiRequest(payload).then((response) => { if (refresh) { self.update(); } }); }, ignorePopularSearch: function(ignoredSearch, index) { // Remove from display right away because there's a delay when there's a lot of data document.getElementById('searchwp-metrics--hook-popular-' + index).style.display = 'none'; // We are also going to persist the ID so we can un-hide it later (because the whole point of // this is to avoid mutation and in doing so avoid the perf hit) this.inlineIgnores.push(index); // Also remove from the model in the 'background' because we can export from here without refresh for (let i = 0; i < this.popularSearchesDetails.length; i++) { if (ignoredSearch === this.popularSearchesDetails[i].query.query) { Vue.delete(this.popularSearchesDetails, i); break; } } // Persist the ignore this.ignoreSearch(ignoredSearch, false); this.needsRefresh = true; }, ignoreFailedSearch: function(ignoredSearch, engine, index) { // Remove from display right away because there's a delay when there's a lot of data document.getElementById('searchwp-metrics--hook-failed-' + index).style.display = 'none'; // We are also going to persist the ID so we can un-hide it later (because the whole point of // this is to avoid mutation and in doing so avoid the perf hit) this.inlineIgnores.push({ engine: engine, ignoredSearch: ignoredSearch, index: index }); // The data removal will be offloaded to the hideFailedSearches() // Persist the ignore this.ignoreSearch(ignoredSearch, false); this.needsRefresh = true; }, getFailedSearches: function(engine) { for (let i = 0; i < this.failedSearches.length; i++) { if (this.failedSearches[i].engine === engine) { return this.failedSearches[i].data; } } return []; }, getPopularSearches: function(engine) { for (let i = 0; i < this.popularSearches.length; i++) { if (this.popularSearches[i].engine === engine) { return this.popularSearches[i]; } } return []; }, getPopularSearchesPercentage: function(engine, engineIndex) { let totalSearches = this.getTotalSearchesCount( engineIndex ); let totalPopularSearches = this.getPopularSearches(engine).datasets[0].data.reduce((a, b) => a + b, 0); return Math.round(( totalPopularSearches * 100 ) / totalSearches); }, getPopularClicks: function(engine) { for (let i = 0; i < this.popularClicks.length; i++) { if (this.popularClicks[i].engine === engine) { return this.popularClicks[i]; } } return []; }, shuffle: function(a){ for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; }, getInsightsCount: function(engine) { if (!this.getPopularClicks(engine)){ return 0; } if (!this.getPopularClicks(engine).insights){ return 0; } let popular = this.getPopularClicks(engine).insights.popular; let underdogs = this.getPopularClicks(engine).insights.underdogs; let analysis = this.getPopularClicks(engine).insights.analysis; let total = 0; if (underdogs && underdogs.length) { total++; } if (popular && popular.length) { total++; } total += Object.keys(analysis).length; return total; }, getInsights: function(engine, limit = 3){ if (!this.getPopularClicks(engine)){ return []; } if (!this.getPopularClicks(engine).insights){ return []; } let insights = []; let popular = this.getPopularClicks(engine).insights.popular; let underdogs = this.getPopularClicks(engine).insights.underdogs; let analysis = this.getPopularClicks(engine).insights.analysis; if (underdogs && underdogs.length) { insights.push({ type: 'underdog', postCount: underdogs.length, posts: underdogs }); } if (popular && popular.length) { insights.push({ type: 'popular', postCount: popular.length, posts: popular }); } for (let insight in analysis) { if (analysis.hasOwnProperty(insight)) { insights.push({ type: 'analysis', query: analysis[insight].query, clickCount: analysis[insight].clicks, postCount: analysis[insight].posts.length, posts: analysis[insight].posts }); } // We only want a maximum of 3 entries in Insights just so the UI is balanced if (limit > 0 && insights.length > (limit - 1)) { break; } } return insights; }, formatDate: function(date) { let year = date.getFullYear().toString(); let month = date.getMonth() + 1; // getMonth() is zero based ¯\_(ツ)_/¯ month = month < 10 ? '0' + month.toString() : month.toString(); let day = date.getDate() < 10 ? '0' + date.getDate().toString() : date.getDate().toString(); return year + '-' + month + '-' + day; }, updateSearchesOverTime: function(data) { this.searchesOverTime = Vue.SearchwpMetricsFormatForChart(data, { type: 'line', borderWidth: 2, fill: true }); }, updatePopularQueriesOverTime: function(data) { let popularSearches = []; if (data) { data.forEach(function(dataset){ popularSearches.push({ engine: dataset.engine, engineLabel: dataset.engineLabel, labels: dataset.labels, datasets: [{ type: 'doughnut', data: dataset.dataset, backgroundColor: dataset.dataset.map((el, index) =>{ return Vue.SearchwpMetricsGetColor(index); }) }] }); }); } this.popularSearches = popularSearches; }, updatePopularClicksOverTime: function(data) { let popularClicks = []; if (data) { data.forEach(function(dataset){ popularClicks.push({ engine: dataset.engine, engineLabel: dataset.engineLabel, type: 'bar', insights: dataset.insights, datasets: dataset.dataset, labels: dataset.labels }); }); } this.popularClicks = popularClicks; }, updateFailedSearchesOverTime: function(data) { let failedSearches = []; if (data) { data.forEach(function(dataset){ let engineFailedSearches = []; for (let i = 0; i < dataset.labels.length; i++) { engineFailedSearches.push({ query: dataset.labels[i], count: dataset.dataset[i] }); } failedSearches.push({ engine: dataset.engine, engineLabel: dataset.engineLabel, data: engineFailedSearches }); }); } this.failedSearches = failedSearches; }, apiRequest: function(data = {}) { // Requests for metrics share a lot of properties if (!Object.keys(data).length) { data.action = 'searchwp_metrics'; data.limit = 10; data.searches = Keyfinder(this.selectedSearchQueries, 'id'); } data.engines = []; if (this.multiselect.engines.value.length) { this.multiselect.engines.value.forEach(function(engine){ data.engines.push(engine.name); }); } data.after = this.formatDate(this.dateRange[0]); data.before = this.formatDate(this.dateRange[1]); data._ajax_nonce = _SEARCHWP_METRICS_VARS.nonce; return new Promise(function(resolve, reject) { jQuery.post(ajaxurl, data, function(response) { if (response.success) { resolve(response); } else { reject(response); } }); }); }, update: function() { this.loading = true; this.apiRequest().then((response) => { this.updateSearchesOverTime(response.data.searches_over_time); this.updatePopularQueriesOverTime(response.data.popular_queries_over_time); this.updatePopularClicksOverTime(response.data.popular_clicks_over_time); this.updateFailedSearchesOverTime(response.data.failed_searches_over_time); this.ignoredQueries = response.data.ignored_queries; this.averageSearchesPerUser = response.data.average_searches_per_user; this.averageClicksPerSearch = response.data.average_clicks_per_search; this.averageClickRank = response.data.average_click_rank; this.totalClicks = response.data.total_clicks; let engines = []; for (let i = 0; i < this.multiselect.engines.value.length; i++) { engines.push({ name: this.multiselect.engines.value[i].name, label: this.multiselect.engines.value[i].label, color: Vue.SearchwpMetricsGetColor(i) }); } this.sameDate = this.formatDate(this.dateRange[0]) === this.formatDate(this.dateRange[1]); this.engines = engines; this.loading = false; }); }, updateLoaderPosition: function() { let topEl = document.getElementById('wpadminbar'); let leftEl = document.getElementById('adminmenuback'); this.loaderPositionTop = topEl ? topEl.offsetHeight : 0; this.loaderPositionLeft = leftEl ? leftEl.offsetWidth : 0; } }, mounted () { this.updateLoaderPosition(); window.addEventListener('resize', this.updateLoaderPosition); this.update(); }, computed: { noResultsDetailsForExport() { return this.getFailedSearches(this.showingFailedSearches); }, popularSearchesDetailsForExport() { // To effectively populate a CSV-compatible display of popular searches, we need to // enforce the data structure to accommodate the display of click data. const source = this.popularSearchesDetails; let formatted = []; if (!source.length) { return formatted; } source.forEach(function(popularSearch) { // Add a row in the spreadsheet for the search, acting as a heading. formatted.push({ query: popularSearch.query.query, searches: popularSearch.query.searchcount, clickId: '', clickTitle: '', clickCount: '', }); // For each click, we need to make space in the spreadsheet, so we // blank out the search query info and instead fill in click details. if (popularSearch.clicks.length) { popularSearch.clicks.forEach(function(click) { formatted.push({ query: '', searches: '', clickId: click.post_id, clickTitle: click.post_title, clickCount: click.clicks, }); }); } }); return formatted; }, translatedEngineDetailsHeading() { return { template: '<span>' + this.i18n.engineDetailsForTimeline + '</span>', props: ['props'], data () { return { engine: this.props } } } }, translatedNoResultsSearchesEngineHeading() { return { template: '<h4>' + this.i18n.noResultsSearchesEngine + '</h4>', props: ['props'], data () { return { engine: this.props } } } }, translatedNoResultsSearchesEngineNote() { return { template: '<p>' + this.i18n.noResultsSearchesEngineNote + '</p>' } }, translatedClickTrackingNote() { return { template: '<p>' + this.i18n.clickTrackingNote + '</p>' } }, translatedPopularSearchDetailsHeading() { return { template: '<h4>' + this.i18n.popularSearchDetailsEngine + '</h4>', props: ['props'], data () { return { engine: this.props } } } }, translatedInsightsEngineHeading() { return { template: '<h4>' + this.i18n.insightsEngine + '</h4>', props: ['props'], data () { return { engine: this.props } } } }, translatedClearMetricsDataNote() { return { template: '<p>' + this.i18n.clearMetricsDataNote + '</p>' } }, translatedClearIgnoredQueriesNote() { return { template: '<p>' + this.i18n.clearIgnoredQueriesNote + '</p>' } }, translatedLoggingRulesNoteDetails() { return { template: '<span>' + this.i18n.loggingRulesNoteDetails + '</span>' } } }, data () { const that = this; return { canEditSettings: _SEARCHWP_METRICS_VARS.can_edit_settings, clearDataOnUninstall: _SEARCHWP_METRICS_VARS.settings.clear_data_on_uninstall, clickTrackingBuoy: _SEARCHWP_METRICS_VARS.settings.click_tracking_buoy, clickTrackingBuoyApplicable: _SEARCHWP_METRICS_VARS.settings.click_tracking_buoy_applicable, inlineIgnores: [], ignoredIps: _SEARCHWP_METRICS_VARS.settings.blocklists.ips, ignoredRoles: _SEARCHWP_METRICS_VARS.settings.blocklists.roles, sameDate: false, showingSettings: false, showingClearMetricsData: false, showingClearIgnoredQueries: false, showingModifyLoggingRules: false, noResultsDetailsJsonFields: { 'Query': 'query', 'No Results Search Count': 'count' }, searchesOverTimeJsonFields: { 'Date': 'label', 'Searches': 'searches' }, engineStatisticsJsonFields: { 'Statistic': 'statistic', 'Value': 'value' }, popularSearchesJsonFields: { 'Search Query': 'query.query', 'Searches': 'query.searchcount' }, popularSearchesDetailsJsonFields: { 'Search Query': 'query', 'Searches': 'searches', 'Clicked Title': { callback: function(value) { return !value.clickId ? '' : value.clickTitle + ' (' + value.clickId + ')'; } }, 'Clicks': 'clickCount' }, popularSearchesCount: 10, popularSearchesDetails: [], loading: true, loaderPositionTop: 0, loaderPositionLeft: 0, showingInsights: false, showingIgnoredSearches: false, showingFailedSearches: false, showingPopularSearchDetails: false, loadingPopularSearchDetails: true, needsRefresh: false, engines: [], ignoredQueries: [], isLoadingSearchSearches: false, searchQueries: [], selectedSearchQueries: [], multiselect: { engines: { value: _SEARCHWP_METRICS_VARS.engine_default, options: _SEARCHWP_METRICS_VARS.engines, } }, averageSearchesPerUser: [], averageClicksPerSearch: [], averageClickRank: [], totalClicks: [], // Metrics are grouped by chart type because some will combine engines while others will not searchesOverTime: null, searchesOverTimeOptions: { maintainAspectRatio: false, legend: {}, tooltips: { cornerRadius: 2, titleMarginBottom: 10, xPadding: 16, yPadding: 9, displayColors: false // We want the fill color to be semi-transparent but that doesn't translate well here } }, popularSearchDetailsClicksOptions: { maintainAspectRatio: false, height: '40px', legend: { display: false }, tooltips: { enabled: false }, hover: { mode: null }, scales: { xAxes: [{ stacked: true, display: false, ticks: { max: 100 } }], yAxes: [{ stacked: true, display: false }] } }, popularSearches: [], popularSearchesOptions: { maintainAspectRatio: true, legend: { display: false }, tooltips: { cornerRadius: 2, titleMarginBottom: 10, xPadding: 11, yPadding: 9, }// , // onClick: function(e,i){ // that.showPopularSearchDetails('default', i[0]['_index']); // In progress, needs to pass engine // } }, popularClicks: [], popularClicksOptions: { maintainAspectRatio: false, legend: { display: false, position: 'bottom' }, tooltips: { cornerRadius: 2, titleMarginBottom: 10, xPadding: 11, yPadding: 9, }, scales: { xAxes: [{ stacked: true, }], yAxes: [{ stacked: true }] } }, failedSearches: [], dateRange: [ new Date(_SEARCHWP_METRICS_VARS.options.default_start), new Date(_SEARCHWP_METRICS_VARS.options.default_end) ], i18n: { addAsPartialMatch: _SEARCHWP_METRICS_VARS.i18n.add_as_partial_match, areYouSure: _SEARCHWP_METRICS_VARS.i18n.are_you_sure, averageClickRank: _SEARCHWP_METRICS_VARS.i18n.average_click_rank, averageRank: _SEARCHWP_METRICS_VARS.i18n.average_rank, cancel: _SEARCHWP_METRICS_VARS.i18n.cancel, chooseEngine: _SEARCHWP_METRICS_VARS.i18n.choose_engine, clearData: _SEARCHWP_METRICS_VARS.i18n.clear_data, clearIgnoredQueriesNote: _SEARCHWP_METRICS_VARS.i18n.clear_ignored_queries_note, clearMetricsData: _SEARCHWP_METRICS_VARS.i18n.clear_metrics_data, clearMetricsDataNote: _SEARCHWP_METRICS_VARS.i18n.clear_metrics_data_note, clickedResult: _SEARCHWP_METRICS_VARS.i18n.clicked_result, clicks: _SEARCHWP_METRICS_VARS.i18n.clicks, clicksPerSearch: _SEARCHWP_METRICS_VARS.i18n.clicks_per_search, clickTrackingNote: _SEARCHWP_METRICS_VARS.i18n.click_tracking_note, clickTrackingBuoy: _SEARCHWP_METRICS_VARS.i18n.click_tracking_buoy, clickTrackingBuoyLabelNote: _SEARCHWP_METRICS_VARS.i18n.click_tracking_buoy_label_note, clickTrackingBuoyUnavailable: _SEARCHWP_METRICS_VARS.i18n.click_tracking_buoy_unavailable, conversionRate: _SEARCHWP_METRICS_VARS.i18n.conversion_rate, datePicker: { dow: _SEARCHWP_METRICS_VARS.options.first_day_of_week, hourTip: _SEARCHWP_METRICS_VARS.i18n.select_hour, minuteTip: _SEARCHWP_METRICS_VARS.i18n.select_minute, secondTip: _SEARCHWP_METRICS_VARS.i18n.select_second, yearSuffix: _SEARCHWP_METRICS_VARS.options.year_suffix, monthsHead: _SEARCHWP_METRICS_VARS.i18n.months.split('_'), months: _SEARCHWP_METRICS_VARS.i18n.months_abbr.split('_'), weeks: _SEARCHWP_METRICS_VARS.i18n.days_abbr.split('_'), cancelTip: _SEARCHWP_METRICS_VARS.i18n.close, submitTip: _SEARCHWP_METRICS_VARS.i18n.update }, dateRange: _SEARCHWP_METRICS_VARS.i18n.date_range, engineDetailsForTimeline: _SEARCHWP_METRICS_VARS.i18n.engine_details_for_timeline, engineStatistics: _SEARCHWP_METRICS_VARS.i18n.engine_statistics, enginesToDisplay: _SEARCHWP_METRICS_VARS.i18n.engines_to_display, entry: _SEARCHWP_METRICS_VARS.i18n.entry, exportEngineStatistics: _SEARCHWP_METRICS_VARS.i18n.export_engine_statistics, exportPopularSearches: _SEARCHWP_METRICS_VARS.i18n.export_popular_searches, exportSearchesOverTime: _SEARCHWP_METRICS_VARS.i18n.export_searches_over_time, ignored: _SEARCHWP_METRICS_VARS.i18n.ignored, ignoredSearches: _SEARCHWP_METRICS_VARS.i18n.ignored_searches, ignoredSearchQuery: _SEARCHWP_METRICS_VARS.i18n.ignored_search_query, ignoredMessage: _SEARCHWP_METRICS_VARS.i18n.ignored_message, insights: _SEARCHWP_METRICS_VARS.i18n.insights, insightsEngine: _SEARCHWP_METRICS_VARS.i18n.insights_engine, ipBlocklist: _SEARCHWP_METRICS_VARS.i18n.ip_blocklist, ipBlocklistNote: _SEARCHWP_METRICS_VARS.i18n.ip_blocklist_note, limitMetricsToQueries: _SEARCHWP_METRICS_VARS.i18n.limit_metrics_to_queries, loggingRules: _SEARCHWP_METRICS_VARS.i18n.logging_rules, loggingRulesNote: _SEARCHWP_METRICS_VARS.i18n.logging_rules_note, loggingRulesNoteDetails: _SEARCHWP_METRICS_VARS.i18n.logging_rules_note_details, modifyLoggingRules: _SEARCHWP_METRICS_VARS.i18n.modify_logging_rules, noClicks: _SEARCHWP_METRICS_VARS.i18n.no_clicks, noFailedSearches: _SEARCHWP_METRICS_VARS.i18n.no_failed_searches, noIgnoredQueries: _SEARCHWP_METRICS_VARS.i18n.no_ignored_queries, noInsights: _SEARCHWP_METRICS_VARS.i18n.no_insights, noResultsSearches: _SEARCHWP_METRICS_VARS.i18n.no_results_searches, noResultsSearchesEngine: _SEARCHWP_METRICS_VARS.i18n.no_results_searches_engine, noResultsSearchesEngineNote: _SEARCHWP_METRICS_VARS.i18n.no_results_searches_engine_note, notEnoughData: _SEARCHWP_METRICS_VARS.i18n.not_enough_data, ofAllSearches: _SEARCHWP_METRICS_VARS.i18n.of_all_searches, popularSearchDetailsEngine: _SEARCHWP_METRICS_VARS.i18n.popular_search_details_engine, popularSearchDetailsNote: _SEARCHWP_METRICS_VARS.i18n.popular_search_details_note, popularSearches: _SEARCHWP_METRICS_VARS.i18n.popular_searches, removeAllIgnoredQueries: _SEARCHWP_METRICS_VARS.i18n.remove_all_ignored_queries, removeOnUninstallation: _SEARCHWP_METRICS_VARS.i18n.remove_on_uninstallation, removeOnUninstallationLabelNote: _SEARCHWP_METRICS_VARS.i18n.remove_on_uninstallation_label_note, saveClose: _SEARCHWP_METRICS_VARS.i18n.save_close, searches: _SEARCHWP_METRICS_VARS.i18n.searches, searchesPerUser: _SEARCHWP_METRICS_VARS.i18n.searches_per_user, searchesPerUserNote: _SEARCHWP_METRICS_VARS.i18n.searches_per_user_note, // searchMetrics: _SEARCHWP_METRICS_VARS.i18n.search_metrics, searchQuery: _SEARCHWP_METRICS_VARS.i18n.search_query, searchQueryControls: _SEARCHWP_METRICS_VARS.i18n.search_query_controls, settings: _SEARCHWP_METRICS_VARS.i18n.settings, stopIgnoringQuery: _SEARCHWP_METRICS_VARS.i18n.stop_ignoring_query, to: _SEARCHWP_METRICS_VARS.i18n.to, totalResultsViewed: _SEARCHWP_METRICS_VARS.i18n.total_results_viewed, totalSearches: _SEARCHWP_METRICS_VARS.i18n.total_searches, userIdRoleBlocklist: _SEARCHWP_METRICS_VARS.i18n.user_id_role_blocklist, userIdRoleBlocklistNote: _SEARCHWP_METRICS_VARS.i18n.user_id_role_blocklist_note, viewAll: _SEARCHWP_METRICS_VARS.i18n.view_all, viewMore: _SEARCHWP_METRICS_VARS.i18n.view_more, viewNoResultsSearches: _SEARCHWP_METRICS_VARS.i18n.view_no_results_searches } } } } </script> <style lang="scss"> .fade-enter-active, .fade-leave-active { transition: opacity 135ms; } .fade-enter, .fade-leave-to { opacity: 0; } .searchwp-metrics { margin-right: 20px; // Match the gap from the Admin Menu * { box-sizing: border-box; } // Date picker hard codes a width of 403px but in range mode // with buttons it prevents them from sitting next to each other .datepicker-range { width: 100%; min-width: 1px; // These styles aim to match this input with the multiselect inputs > input { background: #fff; border-radius: 5px; border: 1px solid #e8e8e8; font-size: 16px; line-height: 20px; padding: 8px 10px; margin-top: -1px; height: 40px; } .datepicker-popup{ width: 100vw; max-width: 415px; } } .modal-vue-wrapper { z-index: 999999 !important; .modal-vue-overlay { background: rgba( 30, 30, 30, 0.5 ) !important; } .modal-vue-panel { max-width: 600px; overflow-y: scroll; .modal-vue-content { padding-right: 1em; } // also need to reset the webkit customizations // &::-webkit-scrollbar, // &::-webkit-scrollbar-track, // &::-webkit-scrollbar-thumb { // all: initial !important; // } } } .searchwp-metrics__engine-popular-searches { .searchwp-metrics__modal, .modal-vue-panel { max-width: 800px; } } } .searchwp-metrics__loading { position: fixed; top: 0; right: 0; left: 0; bottom: 0; z-index: 99999; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.85); } .searchwp-is-clickable { cursor: pointer; } .searchwp-metrics__controls { width: 100%; display: flex; justify-content: space-between; flex-direction: row; margin: 1% 0 2% 0; .searchwp-metrics__control { width: 32%; } } .searchwp-metrics__control { padding: 1.5em; background: #fff; h4 { font-size: 1.1em; margin: 0 0 0.7em 1px; } } .searchwp-metrics__searches-over-time { .searchwp-metrics-chart, .searchwp-metrics-chart canvas { height: 300px !important; // Manually setting this isn't working } } .searchwp-metrics__searches-over-time, .searchwp-metrics__engine-details { width: 100%; padding: 1.5em; background: #fff; margin: 2% 0; } .searchwp-metrics__engine-details { > div { display: flex; &.searchwp-metrics__engine-details-hook > div { padding-right: 2%; padding-left: 2%; &:first-child { padding-left: 0; } &:last-child { padding-right: 0; } } } } .searchwp-metrics__engine-details-heading-group { display: flex; justify-content: space-between; margin-bottom: 1.6em; align-items: center; button.button { padding: 0 0.3em; line-height: 1; display: inline-block; span { opacity: 0.5; } } } .searchwp-metrics__engine-details-heading { margin-top: 0; margin-bottom: 0; font-size: 1.7em; color: #444; display: flex; align-items: center; width: 100%; } .searchwp-metrics__engine-details-legend { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 0.25em; } .searchwp-metrics__engine-popular-searches { width: 35%; } .searchwp-metrics__engine-top-clicks { flex: 1; .searchwp-metrics-table { margin-top: 2em; } } .searchwp-metrics__chart-donut-wrapper { display: flex; > * { display: flex; flex-direction: column; &:first-child { flex: 1; padding-right: 3em; } &:last-child { align-items: center; padding-top: 1em; } } } .searchwp-metrics__engine-popular-searches-coverage { padding-top: 1.5em; text-align: center; span { font-size: 2.5em; font-weight: bold; letter-spacing: -1px; display: block; line-height: 1; } } .searchwp-delete { opacity: 0.5; color: #aa0000; transform: scale(0.8); } .searchwp-metrics-table { width: 100%; border-collapse: collapse; td { padding: 0.2em 0; } td:first-of-type { width: 100%; position: relative; } .searchwp-delete { opacity: 0; } tr:hover .searchwp-delete { opacity: 0.5; } } .searchwp-metrics__control-queries, .searchwp-metrics__heading-button { display: flex; justify-content: space-between; align-items: center; > .button { margin-left: 1em; cursor: pointer; } } .searchwp-popular-clicks-suggestions { overflow: auto; padding: 1.5em; ul > li + li { border-top: 2px solid #efefef; margin-top: 2em; padding-top: 0.5em; } h6 { font-size: 14px; margin-top: 1em; margin-bottom: 0.25em; } } .searchwp-metrics__engine-details-alpha { width: 30%; position: relative; padding-left: 2px !important; // visual illusion of alignment with the legend indicator } .searchwp-metrics__engine-details-beta { width: 36%; position: relative; } .searchwp-metrics__engine-details-beta { padding-left: 3%; padding-right: 3%; &:before, &:after { top: 0; bottom: 0; background: #dcdcdc; content: ''; display: block; position: absolute; width: 1px; } &:before { left: -1.5%; } &:after { right: 0; } } .searchwp-metrics__engine-details-omega { width: auto; flex: 1; } .searchwp-metrics__stats-grid { display: flex; justify-content: space-between; flex-wrap: wrap; > * { width: 48%; } dl, dt, dd { margin: 0; padding: 0; line-height: 1.5; } dd { font-weight: bold; font-size: 2.8em; letter-spacing: -1px; line-height: 1; padding-top: 0.15em; margin-bottom: 0.6em; } } .searchwp-trigger__external { border: 0; padding: 0; margin: 0; background: transparent; color: #00a0d2; cursor: pointer; display: flex; * { display: flex; justify-content: center; align-items: center; } } .searchwp-metrics__flex { display: flex; align-items: center; > * { padding-left: 0.25em; &:first-child { padding-left: 0; } } } .searchwp-metrics__modal { max-width: 600px; text-align: left; table { width: 100%; border-collapse: collapse; font-size: 0.9em; margin-top: 1em; th { padding-bottom: 0.7em; } td { border-top: 1px solid #eaeaea; padding: 0.5em 0 0.4em; } .searchwp-delete { position: relative; top: 1px; } } } .searchwp-metrics__modal-confirmation { text-align: center; h4 { margin-top: 0; } .dashicons { color: #e15759; transform: scale(1.2); line-height: 1.2; } } .searchwp-metrics__modal-confirmation--actions { list-style: none; margin: 0; padding: 1em 0 0; .button { margin-left: 0 !important; // TODO: specificity with .searchwp-metrics__title margin-bottom: 1em; } .searchwp-metrics-nonbutton { font-size: 0.83em; } } .searchwp-metrics__engine-popular-searches { .searchwp-metrics-legend-label { span:hover { cursor: pointer; color: #0073aa; text-decoration: underline; } } } .searchwp-metrics__engine-details--heading { display: flex; justify-content: space-between; align-items: center; margin-top: 0.4em; margin-bottom: 1.6em; > h4 { margin: 0.3em 0; font-size: 1.2em; } > div { > input { display: inline-block; width: 3em; } } } .searchwp-metrics__no-data { p { font-style: italic; width: 100%; text-align: center; font-size: 1.5em; padding: 2em 0; } } .searchwp-metrics__note { display: flex; padding: 0.8em; border: 1px solid #dcdcdc; border-radius: 2px; background: #f3f3f3; > span { width: 1em; height: 1em; } > div { flex: 1; padding-left: 0.5em; > *:first-child { margin-top: 0; padding-top: 0; } > *:last-child { margin-bottom: 0; padding-bottom: 0; } } } .searchwp-metrics-nonbutton { border: 0; margin: 0; display: inline-block; text-decoration: underline; color: #0073aa; padding: 0; background: transparent; cursor: pointer; &:hover { color: #00a0d2; } } .searchwp-metrics__engine-suggestions-insights { ul { margin: -0.5em 0 0; padding: 0; list-style: none; } } .searchwp-metrics__engine-suggestions-insight { margin: 1em 0 1.5em; } // Currently not in use... .searchwp-metrics__engine-suggestions-insights-actions { display: flex; padding-left: 1.5em; } .tooltip.popover .searchwp-metrics-popover { .popover-arrow { border-color: #414141; } .popover-inner { background: #414141; color: #fff; border-radius: 3px; padding: 0; box-shadow: none; ul { text-align: left; margin: 0; padding: 0.6em 0; list-style: none; min-width: 100px; } li { padding: 0.3em 0.7em; line-height: 1.5; margin: 0; cursor: pointer; &:hover { background: #159FD2; } } } } .searchwp-metrics__insight { display: flex; justify-content: space-between; > span { display: inline-block; width: 1em; height: 1em; position: relative; left: -0.25em; } } .searchwp-metrics__engine-suggestions-insight-content { flex: 1; p { margin: 0; } } .v-collapse-content{ max-height: 0; transition: max-height 0.3s ease-out; overflow: hidden; padding: 0; } .v-collapse-content-end { transition: max-height 0.3s ease-in; max-height: 1500px; } .searchwp-metrics__accordion { .vc-collapse { padding: 0.4em 1em 0.1em 0.2em; border: 1px solid #dcdcdc; border-radius: 2px; background: #fafafa; margin-bottom: 0.7em; } } .searchwp-metrics__accordion--header { display: flex; justify-content: space-between; align-items: center; text-align: left; } .searchwp-metrics__accordion--trigger { cursor: pointer; flex: 1; display: flex; justify-content: space-between; } .searchwp-metrics__accordion--actions { width: 1.5em; text-align: right; } .searchwp-metrics__accordion--header-icon { width: 1em; // span:before { // color: #BCBCBC; // Taken from SearchWP core // } .dashicons-sos, .dashicons-awards { transform: scale(0.8); } } .searchwp-metrics__accordion--header-title { flex: 1; padding: 0 1em 0 0.4em; margin: 0; font-size: 0.83em; .searchwp-metrics__insight { padding-bottom: 0.4em; } } .searchwp-metrics__accordion--header-figure { width: 4em; font-size: 0.83em; // match the title above text-align: right; } .searchwp-metrics__accordion--content { a { display: block; position: relative; } .searchwp-metrics-chart, .searchwp-metrics-chart canvas { height: 30px !important; } .searchwp-metrics__no-data { p { font-size: 1.05em; margin-top: 0; padding: 0; } } } .searchwp-metrics--primary-col { width: 50%; } .searchwp-metrics--secondary-col { width: 25%; text-align: center; } .searchwp-metrics__inner { padding: 0.5em 0 0.4em 1.4em; } .searchwp-metrics__popular-search-details { position: relative; .vc-collapse, .searchwp-metrics__accordion--content { padding-right: 0.5em; } } .searchwp-metrics__engine-details--heading { margin-bottom: 0; > div { padding-left: 1em; display: flex; align-items: center; } input { border-radius: 2px; } .button { padding: 0 0.3em; line-height: 1; display: inline-block; margin-left: 0.6em; // height: 27px; // Firefox issue with shadow render having an offset line-height: 1; span { opacity: 0.5; } } div.button { display: flex; align-items: center; align-content: center; } } .searchwp-metrics__loading-details { position: relative; } .searchwp-metrics__loading-details-container { > * { opacity: 1; } } .searchwp-metrics__loading-details-loader { opacity: 0; position: absolute; width: 0; height: 0; overflow: hidden; } .searchwp-metrics__is-loading-details { .searchwp-metrics__loading-details-container { opacity: 0; } .searchwp-metrics__loading-details-loader { opacity: 1; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%; padding-top: 15vh; } } .searchwp-metrics__split { display: flex; justify-content: space-between; > * { width: 50%; &:last-child { text-align: right; } } } .searchwp-metrics__guide { font-weight: bold; margin: 0; font-size: 1em; } .searchwp-metrics__popular-search-details { min-height: 40vh; h4 { margin-bottom: 0; } .searchwp-metrics__guide { margin: 1em 0 0.6em; } table { tr { > *:last-child { text-align: right; } } } } .searchwp-metrics__engine-suggestions-insights-details { .searchwp-metrics__inner { padding-top: 0; } &.searchwp-metrics__modal table { margin-top: 0.5em; tr { > *:last-child { text-align: right; } } tbody { font-size: 0.95em; } } .searchwp-metrics-chart--bar { margin-top: 0.5em; } } .searchwp-metrics__title { display: flex; justify-content: space-between; align-items: center; h1 { flex: 1; } .v-popover .button { padding: 0 0.3em; line-height: 1; display: inline-block; margin-left: 0.6em; span { opacity: 0.5; } } .modal-vue-wrapper .modal-vue-panel { overflow-y: inherit; } .modal-vue-wrapper .modal-vue-panel .modal-vue-actions { display: none; } } .tooltip-inner .searchwp-metrics-nonbutton { text-decoration: none; color: #fff; } .searchwp-metrics--hidden { display: none !important; } .searchwp-metrics__modal-logging { h4 { margin-top: 0; } .button { margin: 0; } .searchwp-metrics__modal-confirmation--actions { padding: 0; } } .searchwp-metrics__textarea { margin: 1em 0; textarea { resize: none; display: block; width: 100%; font: 1em monospace; height: 5em; } p { margin: 0; font-size: 0.8em; } } .searchwp-metrics__modal-logging, .searchwp-metrics__modal-settings { .searchwp-metrics__modal-confirmation--actions { margin-top: 1em; text-align: center; } } .searchwp-metrics__modal-settings { .searchwp-metrics__note { margin-bottom: 1em; } } .searchwp-metrics__checkbox { display: flex; margin: 0.5em 0; input { display: inline-block; margin: 5px 6px 0 0; } } .searchwp-metrics__checkbox-label { label { font-size: 0.83em; cursor: pointer; } } @media screen and (max-width:1300px) { .searchwp-metrics__engine-details { .searchwp-metrics__engine-details-hook { flex-wrap: wrap; } .searchwp-metrics__engine-details-alpha, .searchwp-metrics__engine-details-beta { width: 50%; } .searchwp-metrics__engine-details-beta { padding-right: 0; &:after { display: none; } } .searchwp-metrics__engine-details-omega { width: 100%; padding-top: 1em; margin-top: 2em; padding-left: 0; border-top: 1px solid #dcdcdc; } } } @media screen and (max-width:1000px) { .searchwp-metrics__controls { flex-direction: column; .searchwp-metrics__control { width: 100%; + .searchwp-metrics__control { padding-top: 0; } } } } @media screen and (max-width:750px) { .searchwp-metrics__engine-details { .searchwp-metrics__engine-details-alpha, .searchwp-metrics__engine-details-beta { width: 100%; } .searchwp-metrics__engine-details-beta { padding-top: 1em; margin-top: 2em; padding-left: 0; border-top: 1px solid #dcdcdc; &:before { display: none; } } } } @media screen and (max-width:440px) { .searchwp-metrics__engine-details-heading-group { display: block !important; .searchwp-metrics__engine-details-heading { line-height: 1.2; margin-bottom: 0.5em; } } .searchwp-metrics__stats-grid, .searchwp-metrics__chart-donut-wrapper { display: block; } } </style>