Add plugin page, plugin list view

This commit is contained in:
Krzysztof Bialoglowicz 2019-08-21 16:30:56 +02:00
parent 771aa61b38
commit 8afe11c032
16 changed files with 664 additions and 11 deletions

41
package-lock.json generated
View file

@ -9409,7 +9409,8 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true
"bundled": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -9427,11 +9428,13 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
"bundled": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -9444,15 +9447,18 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -9555,7 +9561,8 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true
"bundled": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -9565,6 +9572,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -9577,17 +9585,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true
"bundled": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -9604,6 +9615,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -9676,7 +9688,8 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -9686,6 +9699,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -9761,7 +9775,8 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true
"bundled": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -9791,6 +9806,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -9808,6 +9824,7 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -9846,11 +9863,13 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true
"bundled": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true
"bundled": true,
"optional": true
}
}
},

View file

@ -7,6 +7,7 @@ import useNavigator from "@saleor/hooks/useNavigator";
import useUser from "@saleor/hooks/useUser";
import Navigation from "@saleor/icons/Navigation";
import Pages from "@saleor/icons/Pages";
import Plugins from "@saleor/icons/Plugins";
import ProductTypes from "@saleor/icons/ProductTypes";
import ShippingMethods from "@saleor/icons/ShippingMethods";
import SiteSettings from "@saleor/icons/SiteSettings";
@ -16,6 +17,7 @@ import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc";
import { menuListUrl } from "@saleor/navigation/urls";
import { pageListUrl } from "@saleor/pages/urls";
import { pluginsListUrl } from "@saleor/plugins/urls";
import { productTypeListUrl } from "@saleor/productTypes/urls";
import { shippingZonesListUrl } from "@saleor/shipping/urls";
import { siteSettingsUrl } from "@saleor/siteSettings/urls";
@ -24,6 +26,7 @@ import { taxSection } from "@saleor/taxes/urls";
import { PermissionEnum } from "@saleor/types/globalTypes";
import ConfigurationPage, { MenuItem } from "./ConfigurationPage";
<<<<<<< HEAD
export function createConfigurationMenu(intl: IntlShape): MenuItem[] {
return [
{
@ -108,6 +111,79 @@ export function createConfigurationMenu(intl: IntlShape): MenuItem[] {
}
];
}
=======
export const configurationMenu: MenuItem[] = [
{
description: i18n.t("Determine attributes used to create product types"),
icon: <ProductTypes fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PRODUCTS,
title: i18n.t("Attributes"),
url: attributeListUrl()
},
{
description: i18n.t("Define types of products you sell"),
icon: <ProductTypes fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PRODUCTS,
title: i18n.t("Product Types"),
url: productTypeListUrl()
},
{
description: i18n.t("Manage your employees and their permissions"),
icon: <StaffMembers fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_STAFF,
title: i18n.t("Staff Members"),
url: staffListUrl()
},
{
description: i18n.t("Manage how you ship out orders."),
icon: <ShippingMethods fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_SHIPPING,
title: i18n.t("Shipping Methods"),
url: shippingZonesListUrl()
},
{
description: i18n.t("Manage how your store charges tax"),
icon: <Taxes fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PRODUCTS,
title: i18n.t("Taxes"),
url: taxSection
},
{
description: i18n.t("Define how users can navigate through your store"),
icon: <Navigation fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_MENUS,
title: i18n.t("Navigation"),
url: menuListUrl()
},
{
description: i18n.t("View and update your site settings"),
icon: <SiteSettings fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_SETTINGS,
title: i18n.t("Site Settings"),
url: siteSettingsUrl()
},
{
description: i18n.t("Manage and add additional pages"),
icon: <Pages fontSize="inherit" viewBox="0 0 44 44" />,
permission: PermissionEnum.MANAGE_PAGES,
title: i18n.t("Pages"),
url: pageListUrl()
},
{
description: i18n.t("View and update your plugins and their settings."),
icon: (
<Plugins
fontSize="inherit"
viewBox="-8 -5 44 44"
preserveAspectRatio="xMinYMin meet"
/>
),
permission: PermissionEnum.MANAGE_SETTINGS,
title: i18n.t("Plugins"),
url: pluginsListUrl()
}
];
>>>>>>> Add plugin page, plugin list view
export const configurationMenuUrl = "/configuration/";

17
src/icons/Plugins.tsx Normal file
View file

@ -0,0 +1,17 @@
import createSvgIcon from "@material-ui/icons/utils/createSvgIcon";
import React from "react";
export const Plugins = createSvgIcon(
<>
<g>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.12891 6.10352e-05H12.9999V7.80006H18.7418V6.10352e-05H24.6128V7.80006H31.9999V14.6751H29.0967V29.7876H20.7418V33.6876H16.8708V40.0001H14.8708V33.6876H10.9999V29.7876H2.9031V14.6751H-0.00012207V7.80006H7.12891V6.10352e-05ZM9.12891 7.80006H10.9999V2.00006H9.12891V7.80006ZM4.9031 14.6751V27.7876H27.0967V14.6751H4.9031ZM12.9999 29.7876V31.6876H18.7418V29.7876H12.9999ZM22.6128 7.80006V2.00006H20.7418V7.80006H22.6128ZM1.99988 9.80006V12.6751H29.9999V9.80006H1.99988Z"
fill="#06847B"
/>
</g>
</>
);
Plugins.displayName = "Plugins";
export default Plugins;

View file

@ -39,6 +39,7 @@ import { navigationSection } from "./navigation/urls";
import { NotFound } from "./NotFound";
import OrdersSection from "./orders";
import PageSection from "./pages";
import PluginsSection from "./plugins";
import ProductSection from "./products";
import ProductTypesSection from "./productTypes";
import ShippingSection from "./shipping";
@ -171,6 +172,11 @@ const Routes: React.FC = () => {
path="/pages"
component={PageSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PLUGINS]}
path="/plugins"
component={PluginsSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_ORDERS]}
path="/orders"

View file

@ -0,0 +1,167 @@
import Card from "@material-ui/core/Card";
import {
createStyles,
Theme,
withStyles,
WithStyles
} from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow";
import EditIcon from "@material-ui/icons/Edit";
import React from "react";
import Checkbox from "@saleor/components/Checkbox";
import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import i18n from "@saleor/i18n";
import { maybe, renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "@saleor/types";
import { PageList_pages_edges_node } from "../../types/PageList";
export interface PluginListProps extends ListProps, ListActions {
plugins: PageList_pages_edges_node[];
}
const styles = (theme: Theme) =>
createStyles({
[theme.breakpoints.up("lg")]: {
colActive: {},
colName: {},
colAction: {
textAlign: "right",
"& svg": {
color: theme.palette.primary.main
}
}
},
colName: {},
colActive: {},
colAction: {},
link: {
cursor: "pointer"
}
});
const numberOfColumns = 4;
const PluginList = withStyles(styles, { name: "PageList" })(
({
classes,
settings,
plugins,
disabled,
onNextPage,
pageInfo,
onRowClick,
onUpdateListSettings,
onPreviousPage,
isChecked,
selected,
toggle,
toggleAll,
toolbar
}: PluginListProps & WithStyles<typeof styles>) => {
console.log(plugins);
return (
<Card>
<Table>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={plugins}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colName} padding="dense">
{i18n.t("Name", { context: "table header" })}
</TableCell>
<TableCell className={classes.colActive} padding="dense">
{i18n.t("Active", { context: "table header" })}
</TableCell>
<TableCell className={classes.colAction} padding="dense">
{i18n.t("Action", { context: "table header" })}
</TableCell>
</TableHead>
<TableFooter>
<TableRow>
<TablePagination
colSpan={numberOfColumns}
settings={settings}
hasNextPage={
pageInfo && !disabled ? pageInfo.hasNextPage : false
}
onNextPage={onNextPage}
onUpdateListSettings={onUpdateListSettings}
hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
}
onPreviousPage={onPreviousPage}
/>
</TableRow>
</TableFooter>
<TableBody>
{renderCollection(
plugins,
plugin => {
const isSelected = plugin ? isChecked(plugin.id) : false;
return (
<TableRow
hover={!!plugin}
className={!!plugin ? classes.link : undefined}
onClick={plugin ? onRowClick(plugin.id) : undefined}
key={plugin ? plugin.id : "skeleton"}
selected={isSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(plugin.id)}
/>
</TableCell>
<TableCell className={classes.colName}>
{maybe<React.ReactNode>(() => plugin.name, <Skeleton />)}
</TableCell>
<TableCell className={classes.colActive}>
{maybe<React.ReactNode>(
() => (
<StatusLabel
label={plugin.active ? i18n.t("Yes") : i18n.t("No")}
status={plugin.active ? "success" : "error"}
/>
),
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colAction}>
<div onClick={plugin ? onRowClick(plugin.id) : undefined}>
<EditIcon />
</div>
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
{i18n.t("No plugins found")}
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</Card>
);
}
);
PluginList.displayName = "PluginList";
export default PluginList;

View file

@ -0,0 +1,2 @@
export { default } from "./PluginsList";
export * from "./PluginsList";

View file

@ -0,0 +1,55 @@
import Button from "@material-ui/core/Button";
import AddIcon from "@material-ui/icons/Add";
import React from "react";
import AppHeader from "@saleor/components/AppHeader";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import i18n from "@saleor/i18n";
import { ListActions, PageListProps } from "@saleor/types";
import { PageList_pages_edges_node } from "../../types/PageList";
import PluginsList from "../PluginsList/PluginsList";
export interface PluginsListPageProps extends PageListProps, ListActions {
pages: PageList_pages_edges_node[];
onBack: () => void;
}
const PluginsListPage: React.StatelessComponent<PluginsListPageProps> = ({
disabled,
settings,
onBack,
onNextPage,
onPreviousPage,
onRowClick,
onUpdateListSettings,
pageInfo,
plugins,
isChecked,
selected,
toggle,
toggleAll,
toolbar
}) => (
<Container>
<AppHeader onBack={onBack}>{i18n.t("Configuration")}</AppHeader>
<PageHeader title={i18n.t("Plugins")} />
<PluginsList
disabled={disabled}
settings={settings}
plugins={plugins}
onNextPage={onNextPage}
onPreviousPage={onPreviousPage}
onUpdateListSettings={onUpdateListSettings}
onRowClick={onRowClick}
pageInfo={pageInfo}
isChecked={isChecked}
selected={selected}
toggle={toggle}
toggleAll={toggleAll}
toolbar={toolbar}
/>
</Container>
);
PluginsListPage.displayName = "PluginsListPage";
export default PluginsListPage;

View file

@ -0,0 +1,2 @@
export { default } from "./PluginsListPage";
export * from "./PluginsListPage";

47
src/plugins/index.tsx Normal file
View file

@ -0,0 +1,47 @@
import { parse as parseQs } from "qs";
import React from "react";
import { Route, RouteComponentProps, Switch } from "react-router-dom";
import { WindowTitle } from "../components/WindowTitle";
import i18n from "../i18n";
import {
pluginsPath,
pluginsListPath,
PluginsListUrlQueryParams
} from "./urls";
import PluginsDetailsComponent from "./views/PluginsDetails";
import PluginsListComponent from "./views/PluginsList";
const PluginList: React.StatelessComponent<RouteComponentProps<any>> = ({
location
}) => {
const qs = parseQs(location.search.substr(1));
const params: PluginsListUrlQueryParams = qs;
return <PluginsListComponent params={params} />;
};
// const PageDetails: React.StatelessComponent<RouteComponentProps<any>> = ({
// match
// }) => {
// const qs = parseQs(location.search.substr(1));
// const params: PluginsListUrlQueryParams = qs;
// return (
// <PluginsDetailsComponent
// id={decodeURIComponent(match.params.id)}
// params={params}
// />
// );
// };
const Component = () => (
<>
<WindowTitle title={i18n.t("Plugins")} />
<Switch>
<Route exact path={pluginsListPath} component={PluginList} />
{/* <Route path={pluginsPath(":id")} component={PageDetails} /> */}
</Switch>
</>
);
export default Component;

0
src/plugins/mutations.ts Normal file
View file

68
src/plugins/queries.ts Normal file
View file

@ -0,0 +1,68 @@
import gql from "graphql-tag";
import { TypedQuery } from "../queries";
import { PluginDetails, PluginDetailsVariables } from "./types/PluginDetails";
import { PluginsList, PluginsListVariables } from "./types/PluginsList";
export const pluginsFragment = gql`
fragment pluginFragment on PluginConfiguration {
id
name
description
active
}
`;
// export const pluginsDetailsFragment = gql`
// ${pluginsFragment}
// fragment pluginsDetailsFragment on PluginConfiguration {
// ...pluginFragment
// configuration {
// name
// type
// value
// helpText
// label
// }
// }
// `;
const pluginsList = gql`
${pluginsFragment}
query pluginConfigurations(
$first: Int
$after: String
$last: Int
$before: String
) {
pluginConfigurations(
before: $before
after: $after
first: $first
last: $last
) {
edges {
node {
...pluginFragment
}
}
}
}
`;
export const TypedPluginsListQuery = TypedQuery<
PluginsList,
PluginsListVariables
>(pluginsList);
// const pluginsDetails = gql`
// ${pluginsDetailsFragment}
// query pluginConfiguration($id: ID!) {
// page(id: $id) {
// ...pluginsDetailsFragment
// }
// }
// `;
// export const TypedPluginsDetailsQuery = TypedQuery<
// PluginDetails,
// PluginDetailsVariables
// >(pluginsDetails);

View file

@ -0,0 +1,36 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: pluginConfigurations
// ====================================================
export interface pluginConfigurations_pluginConfigurations_edges_node {
__typename: "PluginConfiguration";
id: string;
name: string;
description: string;
active: boolean;
}
export interface pluginConfigurations_pluginConfigurations_edges {
__typename: "PluginConfigurationCountableEdge";
node: pluginConfigurations_pluginConfigurations_edges_node;
}
export interface pluginConfigurations_pluginConfigurations {
__typename: "PluginConfigurationCountableConnection";
edges: pluginConfigurations_pluginConfigurations_edges[];
}
export interface pluginConfigurations {
pluginConfigurations: pluginConfigurations_pluginConfigurations | null;
}
export interface pluginConfigurationsVariables {
first?: number | null;
after?: string | null;
last?: number | null;
before?: string | null;
}

View file

@ -0,0 +1,15 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL fragment: pluginFragment
// ====================================================
export interface pluginFragment {
__typename: "PluginConfiguration";
id: string;
name: string;
description: string;
active: boolean;
}

21
src/plugins/urls.ts Normal file
View file

@ -0,0 +1,21 @@
import { stringify as stringifyQs } from "qs";
import urlJoin from "url-join";
import { BulkAction, Dialog, Pagination, SingleAction } from "../types";
export const pluginsSection = "/plugins/";
export const pluginsListPath = pluginsSection;
export type PluginsListUrlDialog = "remove" | "remove-many";
export type PluginsListUrlQueryParams = BulkAction &
Dialog<PluginsListUrlDialog> &
Pagination &
SingleAction;
export const pluginsListUrl = (params?: PluginsListUrlQueryParams) =>
pluginsListPath + "?" + stringifyQs(params);
export const pluginsPath = (id: string) => urlJoin(pluginsSection, id);
export type PluginsUrlDialog = "edit-item" | "remove";
export type PluginsUrlQueryParams = Dialog<PluginsUrlDialog> & SingleAction;
export const pluginsUrl = (id: string, params?: PluginsUrlQueryParams) =>
pluginsPath(encodeURIComponent(id)) + "?" + stringifyQs(params);

View file

@ -0,0 +1,81 @@
import Button from "@material-ui/core/Button";
import DialogContentText from "@material-ui/core/DialogContentText";
import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete";
import React from "react";
import ActionDialog from "@saleor/components/ActionDialog";
import { configurationMenuUrl } from "@saleor/configuration";
import useBulkActions from "@saleor/hooks/useBulkActions";
import useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import usePaginator, {
createPaginationState
} from "@saleor/hooks/usePaginator";
import i18n from "@saleor/i18n";
import { getMutationState, maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types";
import PluginsListPage from "../components/PluginsListPage/PluginsListPage";
import { TypedPluginsListQuery } from "../queries";
import {
pluginsListUrl,
PluginsListUrlDialog,
PluginsListUrlQueryParams,
pluginsUrl
} from "../urls";
interface PluginsListProps {
params: PluginsListUrlQueryParams;
}
export const PluginsList: React.StatelessComponent<PluginsListProps> = ({
params
}) => {
const navigate = useNavigator();
const notify = useNotifier();
const paginate = usePaginator();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids
);
const { updateListSettings, settings } = useListSettings(
ListViews.PAGES_LIST
);
const paginationState = createPaginationState(settings.rowNumber, params);
return (
<TypedPluginsListQuery displayLoader variables={paginationState}>
{({ data, loading, refetch }) => {
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
maybe(() => data.pages.pageInfo),
paginationState,
params
);
return (
<>
<PluginsListPage
disabled={loading}
settings={settings}
plugins={maybe(() =>
data.pluginConfigurations.edges.map(edge => edge.node)
)}
pageInfo={pageInfo}
onBack={() => navigate(configurationMenuUrl)}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings}
onRowClick={id => () => navigate(pluginsUrl(id))}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
/>
</>
);
}}
</TypedPluginsListQuery>
);
};
export default PluginsList;

View file

@ -26660,6 +26660,47 @@ exports[`Storyshots Views / Configuration default 1`] = `
</div>
</div>
</div>
<div
class="MuiPaper-root-id MuiPaper-elevation1-id MuiPaper-rounded-id MuiCard-root-id ConfigurationPage-card-id"
>
<div
class="MuiCardContent-root-id ConfigurationPage-cardContent-id"
>
<div
class="ConfigurationPage-icon-id"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root-id MuiSvgIcon-fontSizeInherit-id"
focusable="false"
preserveAspectRatio="xMinYMin meet"
role="presentation"
viewBox="-8 -5 44 44"
>
<g>
<path
clip-rule="evenodd"
d="M7.12891 6.10352e-05H12.9999V7.80006H18.7418V6.10352e-05H24.6128V7.80006H31.9999V14.6751H29.0967V29.7876H20.7418V33.6876H16.8708V40.0001H14.8708V33.6876H10.9999V29.7876H2.9031V14.6751H-0.00012207V7.80006H7.12891V6.10352e-05ZM9.12891 7.80006H10.9999V2.00006H9.12891V7.80006ZM4.9031 14.6751V27.7876H27.0967V14.6751H4.9031ZM12.9999 29.7876V31.6876H18.7418V29.7876H12.9999ZM22.6128 7.80006V2.00006H20.7418V7.80006H22.6128ZM1.99988 9.80006V12.6751H29.9999V9.80006H1.99988Z"
fill="#06847B"
fill-rule="evenodd"
/>
</g>
</svg>
</div>
<div>
<p
class="MuiTypography-root-id MuiTypography-body2-id MuiTypography-colorPrimary-id ConfigurationPage-sectionTitle-id"
>
Plugins
</p>
<p
class="MuiTypography-root-id MuiTypography-body2-id ConfigurationPage-sectionDescription-id"
>
View and update your plugins and their settings.
</p>
</div>
</div>
</div>
</div>
</div>
</div>