Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

339 dashboard use request to get the actual scores #21852

Open
wants to merge 2 commits into
base: 529-create-api-endpoint-for-readability-scores
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions packages/js/src/dashboard/components/dashboard.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import { ReadabilityScores } from "../scores/readability/readability-scores";
import { SeoScores } from "../scores/seo/seo-scores";
import { Scores } from "../scores/components/scores";
import { PageTitle } from "./page-title";

/**
* @type {import("../index").ContentType} ContentType
* @type {import("../index").Features} Features
* @type {import("../index").Endpoints} Endpoints
*/

/**
* @param {ContentType[]} contentTypes The content types.
* @param {string} userName The user name.
* @param {Features} features Whether features are enabled.
* @param {Endpoints} endpoints The endpoints.
* @param {Object<string,string>} headers The headers for the score requests.
* @returns {JSX.Element} The element.
*/
export const Dashboard = ( { contentTypes, userName, features } ) => {
export const Dashboard = ( { contentTypes, userName, features, endpoints, headers } ) => {
return (
<>
<PageTitle userName={ userName } features={ features } />
<div className="yst-flex yst-flex-col @7xl:yst-flex-row yst-gap-6 yst-my-6">
{ features.indexables && features.seoAnalysis && <SeoScores contentTypes={ contentTypes } /> }
{ features.indexables && features.readabilityAnalysis && <ReadabilityScores contentTypes={ contentTypes } /> }
{ features.indexables && features.seoAnalysis && (
<Scores analysisType="seo" contentTypes={ contentTypes } endpoint={ endpoints.seoScores } headers={ headers } />
) }
{ features.indexables && features.readabilityAnalysis && (
<Scores analysisType="readability" contentTypes={ contentTypes } endpoint={ endpoints.readabilityScores } headers={ headers } />
) }
</div>
</>
);
Expand Down
18 changes: 18 additions & 0 deletions packages/js/src/dashboard/fetch/fetch-json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getResponseError } from "./get-response-error";

/**
* @param {string|URL} url The URL to fetch from.
* @param {RequestInit} options The request options.
* @returns {Promise<any|Error>} The promise of a result, or an error.
*/
export const fetchJson = async( url, options ) => {
try {
const response = await fetch( url, options );
if ( ! response.ok ) {
throw getResponseError( response );
}
return response.json();
} catch ( e ) {
return Promise.reject( e );
}
};
14 changes: 14 additions & 0 deletions packages/js/src/dashboard/fetch/get-response-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { TimeoutError } from "./timeout-error";

/**
* @param {Response} response The response.
* @returns {Error} The error that corresponds to the response.
*/
export const getResponseError = ( response ) => {
switch ( response.status ) {
case 408:
return new TimeoutError( "request timed out" );
default:
return new Error( "not ok" );
}
};
12 changes: 12 additions & 0 deletions packages/js/src/dashboard/fetch/timeout-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Represents a timeout error.
*/
export class TimeoutError extends Error {
/**
* @param {string} message The error message.
*/
constructor( message ) {
super( message );
this.name = "TimeoutError";
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "@wordpress/element";
import { debounce, identity } from "lodash";
import { FETCH_DELAY } from "../../shared-admin/constants";
import { fetchJson } from "./fetch-json";

/**
* @typedef {Object} FetchResult
Expand All @@ -16,24 +17,6 @@ import { FETCH_DELAY } from "../../shared-admin/constants";
* @returns {Promise<any|Error>} The promise of a result, or an error.
*/

/**
* @param {string|URL} url The URL to fetch from.
* @param {RequestInit} options The request options.
* @returns {Promise<any|Error>} The promise of a result, or an error.
*/
const fetchJson = async( url, options ) => {
try {
const response = await fetch( url, options );
if ( ! response.ok ) {
// From the perspective of the results, we want to reject this as an error.
throw new Error( "Not ok" );
}
return response.json();
} catch ( error ) {
return Promise.reject( error );
}
};

/**
* @param {any[]} dependencies The dependencies for the fetch.
* @param {string|URL} url The URL to fetch from.
Expand Down
6 changes: 6 additions & 0 deletions packages/js/src/dashboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,9 @@ export { Dashboard } from "./components/dashboard";
* @property {boolean} seoAnalysis Whether SEO analysis is enabled.
* @property {boolean} readabilityAnalysis Whether readability analysis is enabled.
*/

/**
* @typedef {Object} Endpoints The endpoints.
* @property {string} seoScores The endpoint for SEO scores.
* @property {string} readabilityScores The endpoint for readability scores.
*/
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ import { maxBy } from "lodash";
export const ContentStatusDescription = ( { scores, descriptions } ) => {
const maxScore = maxBy( scores, "amount" );

return <p className="yst-my-6">{ descriptions[ maxScore.name ] || "" }</p>;
return <p>{ descriptions[ maxScore?.name ] || "" }</p>;
};
6 changes: 3 additions & 3 deletions packages/js/src/dashboard/scores/components/score-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { ScoreList } from "./score-list";
*/
const ScoreContentSkeletonLoader = () => (
<>
<SkeletonLoader className="yst-w-full yst-my-6">&nbsp;</SkeletonLoader>
<div className="yst-grid yst-grid-cols-1 @md:yst-grid-cols-7 yst-gap-6">
<SkeletonLoader className="yst-w-full">&nbsp;</SkeletonLoader>
<div className="yst-grid yst-grid-cols-1 @md:yst-grid-cols-7 yst-gap-6 yst-mt-6">
<ul className="yst-col-span-4">
{ Object.entries( SCORE_META ).map( ( [ name, { label } ] ) => (
<li
Expand Down Expand Up @@ -51,7 +51,7 @@ export const ScoreContent = ( { scores = [], isLoading, descriptions } ) => {
return (
<>
<ContentStatusDescription scores={ scores } descriptions={ descriptions } />
<div className="yst-grid yst-grid-cols-1 @md:yst-grid-cols-7 yst-gap-6">
<div className="yst-grid yst-grid-cols-1 @md:yst-grid-cols-7 yst-gap-6 yst-mt-6">
{ scores && <ScoreList scores={ scores } /> }
{ scores && <ScoreChart scores={ scores } /> }
</div>
Expand Down
136 changes: 136 additions & 0 deletions packages/js/src/dashboard/scores/components/scores.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { createInterpolateElement, useEffect, useState } from "@wordpress/element";
import { __, sprintf } from "@wordpress/i18n";
import { Alert, Link, Paper, Title } from "@yoast/ui-library";
import { useFetch } from "../../fetch/use-fetch";
import { SCORE_DESCRIPTIONS } from "../score-meta";
import { ContentTypeFilter } from "./content-type-filter";
import { ScoreContent } from "./score-content";
import { TermFilter } from "./term-filter";

/**
* @type {import("../index").ContentType} ContentType
* @type {import("../index").Taxonomy} Taxonomy
* @type {import("../index").Term} Term
* @type {import("../index").AnalysisType} AnalysisType
*/

/**
* @param {string|URL} endpoint The endpoint.
* @param {ContentType} contentType The content type.
* @param {Term?} [term] The term.
* @returns {URL} The URL to get scores.
*/
const createScoresUrl = ( endpoint, contentType, term ) => {
const searchParams = new URLSearchParams( { contentType: contentType.name } );
if ( contentType.taxonomy?.name && term?.name ) {
searchParams.set( "taxonomy", contentType.taxonomy.name );
searchParams.set( "term", term.name );
}
return new URL( "?" + searchParams, endpoint );
};

// Added dummy space as content to prevent children prop warnings in the console.
const supportLink = <Link variant="error" href="admin.php?page=wpseo_page_support"> </Link>;

const TimeoutErrorMessage = createInterpolateElement(
sprintf(
/* translators: %1$s and %2$s expand to an opening/closing tag for a link to the support page. */
__( "A timeout occurred, possibly due to a large number of posts or terms. In case you need further help, please take a look at our %1$sSupport page%2$s.", "wordpress-seo" ),
"<supportLink>",
"</supportLink>"
),
{
supportLink,
}
);
const OtherErrorMessage = createInterpolateElement(
sprintf(
/* translators: %1$s and %2$s expand to an opening/closing tag for a link to the support page. */
__( "Something went wrong. In case you need further help, please take a look at our %1$sSupport page%2$s.", "wordpress-seo" ),
"<supportLink>",
"</supportLink>"
),
{
supportLink,
}
);

/**
* @param {Error?} [error] The error.
* @returns {JSX.Element} The element.
*/
const ErrorAlert = ( { error } ) => {
if ( ! error ) {
return null;
}
return (
<Alert variant="error">
{ error?.name === "TimeoutError"
? TimeoutErrorMessage
: OtherErrorMessage
}
</Alert>
);
};

/**
* @param {AnalysisType} analysisType The analysis type. Either "seo" or "readability".
* @param {ContentType[]} contentTypes The content types. May not be empty.
* @param {string} endpoint The endpoint or base URL.
* @param {Object<string,string>} headers The headers to send with the request.
* @returns {JSX.Element} The element.
*/
export const Scores = ( { analysisType, contentTypes, endpoint, headers } ) => {
const [ selectedContentType, setSelectedContentType ] = useState( contentTypes[ 0 ] );
const [ selectedTerm, setSelectedTerm ] = useState();

const { data: scores, error, isPending } = useFetch( {
dependencies: [ selectedContentType.name, selectedContentType?.taxonomy, selectedTerm?.name ],
url: createScoresUrl( endpoint, selectedContentType, selectedTerm ),
options: {
headers: {
"Content-Type": "application/json",
...headers,
},
},
fetchDelay: 0,
} );

useEffect( () => {
// Reset the selected term when the selected content type changes.
setSelectedTerm( undefined ); // eslint-disable-line no-undefined
}, [ selectedContentType.name ] );

return (
<Paper className="yst-@container yst-grow yst-max-w-screen-sm yst-p-8">
<Title as="h2">
{ analysisType === "readability"
? __( "Readability scores", "wordpress-seo" )
: __( "SEO scores", "wordpress-seo" )
}
</Title>
<div className="yst-grid yst-grid-cols-1 @md:yst-grid-cols-2 yst-gap-6 yst-mt-4">
<ContentTypeFilter
idSuffix={ analysisType }
contentTypes={ contentTypes }
selected={ selectedContentType }
onChange={ setSelectedContentType }
/>
{ selectedContentType.taxonomy && selectedContentType.taxonomy?.links?.search &&
<TermFilter
idSuffix={ analysisType }
taxonomy={ selectedContentType.taxonomy }
selected={ selectedTerm }
onChange={ setSelectedTerm }
/>
}
</div>
<div className="yst-mt-6">
<ErrorAlert error={ error } />
{ ! error && (
<ScoreContent scores={ scores } isLoading={ isPending } descriptions={ SCORE_DESCRIPTIONS[ analysisType ] } />
) }
</div>
</Paper>
);
};
24 changes: 14 additions & 10 deletions packages/js/src/dashboard/scores/components/term-filter.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import { useCallback, useState } from "@wordpress/element";
import { __ } from "@wordpress/i18n";
import { AutocompleteField, Spinner } from "@yoast/ui-library";
import { useFetch } from "../../hooks/use-fetch";
import { useFetch } from "../../fetch/use-fetch";

/**
* @type {import("../index").Taxonomy} Taxonomy
* @type {import("../index").Term} Term
*/

/**
* @param {string|URL} baseUrl The URL to fetch from.
* @param {string|URL} endpoint The URL to fetch from.
* @param {string} query The query.
* @returns {URL} The URL with the query.
* @returns {URL} The URL to query for the terms.
*/
const createQueryUrl = ( baseUrl, query ) => new URL( "?" + new URLSearchParams( {
const createQueryUrl = ( endpoint, query ) => new URL( "?" + new URLSearchParams( {
search: query,
_fields: [ "name", "slug" ],
} ), baseUrl );
_fields: [ "id", "name" ],
} ), endpoint );

/**
* @param {{name: string, slug: string}} term The term from the response.
* @returns {Term} The transformed term.
* @param {{id: number, name: string}} term The term from the response.
* @returns {Term} The transformed term for internal usage.
*/
const transformTerm = ( term ) => ( { name: term.slug, label: term.name } );
const transformTerm = ( term ) => ( { name: String( term.id ), label: term.name } );

/**
* Renders either a list of terms or a message that nothing was found.
Expand Down Expand Up @@ -54,7 +54,11 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => {
const { data: terms = [], error, isPending } = useFetch( {
dependencies: [ taxonomy.links.search, query ],
url: createQueryUrl( taxonomy.links.search, query ),
options: { headers: { "Content-Type": "application/json" } },
options: {
headers: {
"Content-Type": "application/json",
},
},
prepareData: ( result ) => result.map( transformTerm ),
} );

Expand Down
Loading