* Add tabs to apps page

* Detect app in tunnel

* Disable manifest install if Saleor Apps context

* Show only app origin instead full manifest

* Add copy manifest feature

* Update tests

* Add ENV with marketplace endpoint for fetching saleor apps

* Fetch Saleor apps for Marketplace and display them in the tab, otherwise hide

* Code review fixes

* Extract translation messages

* Update tests
This commit is contained in:
Lukasz Ostrowski 2022-10-20 13:35:31 +02:00 committed by GitHub
parent 5c48f5387e
commit d67e4799b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 522 additions and 261 deletions

View file

@ -25,7 +25,8 @@ greatly reduce the amount of work needed to review your work. -->
Modify API_URI if you want test instance to use custom backend. CYPRESS_API_URI is optional, use when necessary. --> Modify API_URI if you want test instance to use custom backend. CYPRESS_API_URI is optional, use when necessary. -->
API_URI=https://automation-dashboard.staging.saleor.cloud/graphql/ API_URI=https://automation-dashboard.staging.saleor.cloud/graphql/
MARKETPLACE_URL=https://marketplace-gray.vercel.app/ MARKETPLACE_URL=https://apps.saleor.io
SALEOR_APPS_ENDPOINT=https://apps.saleor.io/api/saleor-apps
### Do you want to run more stable tests? ### Do you want to run more stable tests?
To run all tests, just select the stable checkbox. To speed up tests, increase the number of containers. Tests will be re-run only when the "run e2e" label is added. To run all tests, just select the stable checkbox. To speed up tests, increase the number of containers. Tests will be re-run only when the "run e2e" label is added.

View file

@ -18,6 +18,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MARKETPLACE_URL: "https://apps.saleor.io/" MARKETPLACE_URL: "https://apps.saleor.io/"
SALEOR_APPS_ENDPOINT: "https://apps.saleor.io/api/saleor-apps"
IS_CLOUD_INSTANCE: true IS_CLOUD_INSTANCE: true
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View file

@ -21,6 +21,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MARKETPLACE_URL: "https://apps.saleor.io/" MARKETPLACE_URL: "https://apps.saleor.io/"
SALEOR_APPS_ENDPOINT: "https://apps.saleor.io/api/saleor-apps"
ENVIRONMENT: demo-staging ENVIRONMENT: demo-staging
DEMO_MODE: true DEMO_MODE: true
steps: steps:

View file

@ -16,6 +16,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MARKETPLACE_URL: "https://apps.saleor.io/" MARKETPLACE_URL: "https://apps.saleor.io/"
SALEOR_APPS_ENDPOINT: "https://apps.saleor.io/api/saleor-apps"
ENVIRONMENT: demo ENVIRONMENT: demo
DEMO_MODE: true DEMO_MODE: true
steps: steps:

View file

@ -18,6 +18,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MARKETPLACE_URL: "https://apps.saleor.io/" MARKETPLACE_URL: "https://apps.saleor.io/"
SALEOR_APPS_ENDPOINT: "https://apps.saleor.io/api/saleor-apps"
ENVIRONMENT: saleor-master-staging ENVIRONMENT: saleor-master-staging
IS_CLOUD_INSTANCE: true IS_CLOUD_INSTANCE: true
steps: steps:

View file

@ -23,6 +23,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
MARKETPLACE_URL: "https://apps.saleor.io/" MARKETPLACE_URL: "https://apps.saleor.io/"
SALEOR_APPS_ENDPOINT: "https://apps.saleor.io/api/saleor-apps"
VERSION: ${{ github.event.inputs.git_ref || github.ref_name }} VERSION: ${{ github.event.inputs.git_ref || github.ref_name }}
IS_CLOUD_INSTANCE: true IS_CLOUD_INSTANCE: true
steps: steps:

View file

@ -6,11 +6,12 @@ COPY . .
ARG APP_MOUNT_URI ARG APP_MOUNT_URI
ARG API_URI ARG API_URI
ARG MARKETPLACE_URL ARG MARKETPLACE_URL
ARG SALEOR_APPS_ENDPOINT
ARG STATIC_URL ARG STATIC_URL
ENV API_URI ${API_URI:-http://localhost:8000/graphql/} ENV API_URI ${API_URI:-http://localhost:8000/graphql/}
ENV APP_MOUNT_URI ${APP_MOUNT_URI:-/dashboard/} ENV APP_MOUNT_URI ${APP_MOUNT_URI:-/dashboard/}
ENV STATIC_URL ${STATIC_URL:-/dashboard/} ENV STATIC_URL ${STATIC_URL:-/dashboard/}
RUN STATIC_URL=${STATIC_URL} API_URI=${API_URI} MARKETPLACE_URL=${MARKETPLACE_URL} APP_MOUNT_URI=${APP_MOUNT_URI} npm run build RUN STATIC_URL=${STATIC_URL} API_URI=${API_URI} MARKETPLACE_URL=${MARKETPLACE_URL} SALEOR_APPS_ENDPOINT=${SALEOR_APPS_ENDPOINT} APP_MOUNT_URI=${APP_MOUNT_URI} npm run build
FROM nginx:stable FROM nginx:stable
WORKDIR /app WORKDIR /app

View file

@ -6,6 +6,7 @@ COPY . .
ARG APP_MOUNT_URI ARG APP_MOUNT_URI
ARG API_URI ARG API_URI
ARG MARKETPLACE_URL ARG MARKETPLACE_URL
ARG SALEOR_APPS_ENDPOINT
ARG STATIC_URL ARG STATIC_URL
ENV API_URI ${API_URI:-http://localhost:8000/graphql/} ENV API_URI ${API_URI:-http://localhost:8000/graphql/}
ENV APP_MOUNT_URI ${APP_MOUNT_URI:-/} ENV APP_MOUNT_URI ${APP_MOUNT_URI:-/}

View file

@ -97,6 +97,9 @@
"context": "dialog header", "context": "dialog header",
"string": "Change Password" "string": "Change Password"
}, },
"+niGip": {
"string": "Saleor Apps"
},
"+pLi+M": { "+pLi+M": {
"context": "issued by app label", "context": "issued by app label",
"string": "Issued by app" "string": "Issued by app"
@ -2013,6 +2016,9 @@
"context": "change warehouse dialog warehouse list label", "context": "change warehouse dialog warehouse list label",
"string": "Warehouses A to Z" "string": "Warehouses A to Z"
}, },
"EqDdoh": {
"string": "Local apps are custom webhooks & token pairs that can be used to connect apps and access Saleor API. Read more."
},
"ErNH3D": { "ErNH3D": {
"string": "Define where this attribute should be used in Saleor system" "string": "Define where this attribute should be used in Saleor system"
}, },
@ -2077,6 +2083,9 @@
"context": "dialog content", "context": "dialog content",
"string": "Select method you want to use to change address" "string": "Select method you want to use to change address"
}, },
"FLtdaw": {
"string": "Saleor apps are hosted and maintained by Saleor Team. They are preinstalled for you and ready to use"
},
"FNAZoh": { "FNAZoh": {
"string": "Last login" "string": "Last login"
}, },
@ -2646,6 +2655,9 @@
"context": "currency code select", "context": "currency code select",
"string": "{code} - {countries}" "string": "{code} - {countries}"
}, },
"J8frvS": {
"string": "3rd party apps"
},
"JDz5h8": { "JDz5h8": {
"context": "number of subcategories in category", "context": "number of subcategories in category",
"string": "Subcategories" "string": "Subcategories"
@ -3438,6 +3450,10 @@
"context": "attribute list", "context": "attribute list",
"string": "Attribute {number}" "string": "Attribute {number}"
}, },
"PbQJY5": {
"context": "section header",
"string": "Saleor Apps"
},
"PbqNhi": { "PbqNhi": {
"context": "order status", "context": "order status",
"string": "Partially fulfilled" "string": "Partially fulfilled"
@ -3628,6 +3644,9 @@
"context": "number of collections", "context": "number of collections",
"string": "Collections ({quantity})" "string": "Collections ({quantity})"
}, },
"QdQ9z7": {
"string": "(TUNNEL - DEVELOPMENT)"
},
"Qe4XHv": { "Qe4XHv": {
"context": "years after label", "context": "years after label",
"string": "years after issue" "string": "years after issue"
@ -4153,6 +4172,9 @@
"context": "attribute values", "context": "attribute values",
"string": "Value {number}" "string": "Value {number}"
}, },
"UxTSw7": {
"string": "Webhooks & Events"
},
"UxdBmI": { "UxdBmI": {
"context": "collection availability", "context": "collection availability",
"string": "Availability" "string": "Availability"
@ -7398,6 +7420,9 @@
"context": "gift card history message", "context": "gift card history message",
"string": "Gift card tags were updated" "string": "Gift card tags were updated"
}, },
"vkY3W9": {
"string": "Third party apps are installed with App Manifests. They contain UI accessible from dashboard and can extend it. Read more here."
},
"vlLyvk": { "vlLyvk": {
"string": "{inputType} attributes cannot be used as variant selection attributes." "string": "{inputType} attributes cannot be used as variant selection attributes."
}, },

View file

@ -0,0 +1,72 @@
import { Typography } from "@material-ui/core";
import { CopyIcon, makeStyles, Tooltip } from "@saleor/macaw-ui";
import clsx from "clsx";
import React, { useState } from "react";
const useStyles = makeStyles(
theme => ({
"@keyframes pulse": {
from: { transform: "scale(1)" },
to: { transform: "scale(1.2)" },
},
manifestText: {
color: theme.palette.text.secondary,
"&:hover svg": {
visibility: "visible",
},
},
copyIcon: {
marginRight: theme.spacing(1),
visibility: "hidden",
verticalAlign: "middle",
transition: "0.2s",
},
copyIconColorful: {
color: theme.palette.primary.main,
animation: "$pulse 0.2s",
},
}),
{ name: "AppManifestTableDisplay" },
);
interface AppManifestTableDisplayProps {
manifestUrl: string;
}
const getAppDomainFromManifest = (manifest: string) => new URL(manifest).host;
export const AppManifestTableDisplay = ({
manifestUrl,
}: AppManifestTableDisplayProps) => {
const styles = useStyles();
const [copied, setCopied] = useState(false);
return (
<Tooltip placement="top" title={manifestUrl} header="App Manifest URL">
<Typography
onMouseOut={() => setCopied(false)}
className={styles.manifestText}
onClick={e => {
try {
e.stopPropagation();
e.preventDefault();
navigator.clipboard.writeText(manifestUrl);
setCopied(true);
} catch (e) {
// Copy not supported, ignore
}
}}
>
{!!navigator.clipboard && (
<CopyIcon
className={clsx(styles.copyIcon, {
[styles.copyIconColorful]: copied,
})}
/>
)}
{getAppDomainFromManifest(manifestUrl)}
</Typography>
</Tooltip>
);
};

View file

@ -0,0 +1,46 @@
import { PageTab, PageTabs } from "@saleor/macaw-ui";
import React, { ComponentProps } from "react";
import { useIntl } from "react-intl";
export type AppPageTabValue =
| "THIRD_PARTY"
| "WEBHOOKS_AND_EVENTS"
| "SALEOR_APPS";
type AllProps = ComponentProps<typeof PageTabs>;
type AvailableProps = Omit<AllProps, "children" | "onChange" | "value"> & {
value: AppPageTabValue;
showSaleorApps: boolean;
onChange(newValue: AppPageTabValue): void;
};
export const AppPageTabs = ({ showSaleorApps, ...props }: AvailableProps) => {
const intl = useIntl();
return (
<PageTabs {...props}>
<PageTab
value="THIRD_PARTY"
label={intl.formatMessage({
defaultMessage: "3rd party apps",
id: "J8frvS",
})}
/>
<PageTab
value="WEBHOOKS_AND_EVENTS"
label={intl.formatMessage({
defaultMessage: "Webhooks & Events",
id: "UxTSw7",
})}
/>
{showSaleorApps && (
<PageTab
value="SALEOR_APPS"
label={intl.formatMessage({
defaultMessage: "Saleor Apps",
id: "+niGip",
})}
/>
)}
</PageTabs>
);
};

View file

@ -1,11 +1,17 @@
import {
AppPageTabs,
AppPageTabValue,
} from "@saleor/apps/components/AppPageTabs/AppPageTabs";
import { useSaleorApps } from "@saleor/apps/hooks/useSaleorApps";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import { AppsInstallationsQuery, AppsListQuery } from "@saleor/graphql"; import { AppsInstallationsQuery, AppsListQuery } from "@saleor/graphql";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { makeStyles } from "@saleor/macaw-ui";
import { ListProps } from "@saleor/types"; import { ListProps } from "@saleor/types";
import React from "react"; import React, { useEffect, useMemo, useState } from "react";
import { useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import AppsInProgress from "../AppsInProgress/AppsInProgress"; import AppsInProgress from "../AppsInProgress/AppsInProgress";
import CustomApps from "../CustomApps/CustomApps"; import CustomApps from "../CustomApps/CustomApps";
@ -22,6 +28,17 @@ export interface AppsListPageProps extends ListProps {
onAppInstallRetry: (id: string) => void; onAppInstallRetry: (id: string) => void;
} }
const useStyles = makeStyles(
theme => ({
topTabs: {
marginBottom: theme.spacing(4),
},
}),
{
name: "AppsListPageStyles",
},
);
const AppsListPage: React.FC<AppsListPageProps> = ({ const AppsListPage: React.FC<AppsListPageProps> = ({
appsInProgressList, appsInProgressList,
customAppsList, customAppsList,
@ -33,24 +50,73 @@ const AppsListPage: React.FC<AppsListPageProps> = ({
onAppInstallRetry, onAppInstallRetry,
...listProps ...listProps
}) => { }) => {
const {
fetchApps,
apps: fetchedSaleorApps,
saleorAppsEnabled,
} = useSaleorApps();
useEffect(() => {
if (saleorAppsEnabled) {
fetchApps();
}
}, [saleorAppsEnabled, fetchApps]);
const styles = useStyles();
const intl = useIntl(); const intl = useIntl();
const [activeTab, setActiveTab] = useState<AppPageTabValue>("THIRD_PARTY");
const appsInProgress = appsInProgressList?.appsInstallations; const appsInProgress = appsInProgressList?.appsInstallations;
const thirdPartyApps = useMemo(
() =>
installedAppsList?.filter(
app =>
!(fetchedSaleorApps ?? []).find(fetchedApp =>
app.node.manifestUrl.includes(fetchedApp.hostname),
),
),
[installedAppsList, fetchedSaleorApps],
);
const saleorApps = useMemo(
() =>
fetchedSaleorApps
?.map(app =>
installedAppsList?.find(installedApp =>
installedApp.node.manifestUrl.includes(app.hostname),
),
)
.filter(Boolean),
[installedAppsList, fetchedSaleorApps],
);
const renderContent = () => {
switch (activeTab) {
case "THIRD_PARTY": {
return ( return (
<Container> <>
<PageHeader title={intl.formatMessage(sectionNames.apps)} /> <p>
<FormattedMessage
defaultMessage="Third party apps are installed with App Manifests. They contain UI
accessible from dashboard and can extend it. Read more here."
id="vkY3W9"
/>
</p>
<InstalledApps <InstalledApps
appsList={installedAppsList} title={intl.formatMessage({
id: "BvmnJq",
defaultMessage: "Third Party Apps",
description: "section header",
})}
appsList={thirdPartyApps}
onRemove={onInstalledAppRemove} onRemove={onInstalledAppRemove}
displayQuickManifestButton
{...listProps} {...listProps}
/> />
<CardSpacer /> <CardSpacer />
<CustomApps
appsList={customAppsList}
getCustomAppHref={getCustomAppHref}
onRemove={onCustomAppRemove}
/>
{!!appsInProgress?.length && ( {!!appsInProgress?.length && (
<> <>
<CardSpacer /> <CardSpacer />
@ -61,6 +127,63 @@ const AppsListPage: React.FC<AppsListPageProps> = ({
/> />
</> </>
)} )}
</>
);
}
case "WEBHOOKS_AND_EVENTS": {
return (
<>
<p>
<FormattedMessage
defaultMessage="Local apps are custom webhooks & token pairs that can be used to
connect apps and access Saleor API. Read more."
id="EqDdoh"
/>
</p>
<CustomApps
appsList={customAppsList}
getCustomAppHref={getCustomAppHref}
onRemove={onCustomAppRemove}
/>
</>
);
}
case "SALEOR_APPS": {
return (
<>
<p>
<FormattedMessage
defaultMessage="Saleor apps are hosted and maintained by Saleor Team. They are
preinstalled for you and ready to use"
id="FLtdaw"
/>
</p>
<InstalledApps
title={intl.formatMessage({
id: "PbQJY5",
defaultMessage: "Saleor Apps",
description: "section header",
})}
appsList={saleorApps}
onRemove={onInstalledAppRemove}
{...listProps}
/>
</>
);
}
}
};
return (
<Container>
<PageHeader title={intl.formatMessage(sectionNames.apps)} />
<AppPageTabs
showSaleorApps={saleorAppsEnabled}
className={styles.topTabs}
onChange={setActiveTab}
value={activeTab}
/>
{renderContent()}
</Container> </Container>
); );
}; };

View file

@ -6,9 +6,11 @@ import {
TableRow, TableRow,
Typography, Typography,
} from "@material-ui/core"; } from "@material-ui/core";
import { AppManifestTableDisplay } from "@saleor/apps/components/AppManifestTableDisplay/AppManifestTableDisplay";
import { InstallWithManifestFormButton } from "@saleor/apps/components/InstallWithManifestFormButton"; import { InstallWithManifestFormButton } from "@saleor/apps/components/InstallWithManifestFormButton";
import { useAppListContext } from "@saleor/apps/context"; import { useAppListContext } from "@saleor/apps/context";
import { appUrl, createAppInstallUrl } from "@saleor/apps/urls"; import { appUrl, createAppInstallUrl } from "@saleor/apps/urls";
import { isAppInTunnel } from "@saleor/apps/utils";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import { IconButton } from "@saleor/components/IconButton"; import { IconButton } from "@saleor/components/IconButton";
import { TableButtonWrapper } from "@saleor/components/TableButtonWrapper/TableButtonWrapper"; import { TableButtonWrapper } from "@saleor/components/TableButtonWrapper/TableButtonWrapper";
@ -18,9 +20,8 @@ import useNavigator from "@saleor/hooks/useNavigator";
import { DeleteIcon, ResponsiveTable } from "@saleor/macaw-ui"; import { DeleteIcon, ResponsiveTable } from "@saleor/macaw-ui";
import { renderCollection } from "@saleor/misc"; import { renderCollection } from "@saleor/misc";
import { ListProps } from "@saleor/types"; import { ListProps } from "@saleor/types";
import clsx from "clsx";
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage } from "react-intl";
import { useStyles } from "../../styles"; import { useStyles } from "../../styles";
import { AppPermissions } from "../AppPermissions/AppPermissions"; import { AppPermissions } from "../AppPermissions/AppPermissions";
@ -29,14 +30,17 @@ import AppsSkeleton from "../AppsSkeleton";
export interface InstalledAppsProps extends ListProps { export interface InstalledAppsProps extends ListProps {
appsList: AppsListQuery["apps"]["edges"]; appsList: AppsListQuery["apps"]["edges"];
onRemove: (id: string) => void; onRemove: (id: string) => void;
displayQuickManifestButton?: boolean;
title: string;
} }
const InstalledApps: React.FC<InstalledAppsProps> = ({ const InstalledApps: React.FC<InstalledAppsProps> = ({
appsList, appsList,
onRemove, onRemove,
title,
displayQuickManifestButton = false,
...props ...props
}) => { }) => {
const intl = useIntl();
const classes = useStyles(props); const classes = useStyles(props);
const { activateApp, deactivateApp } = useAppListContext(); const { activateApp, deactivateApp } = useAppListContext();
const navigate = useNavigator(); const navigate = useNavigator();
@ -59,15 +63,15 @@ const InstalledApps: React.FC<InstalledAppsProps> = ({
return ( return (
<Card className={classes.apps}> <Card className={classes.apps}>
<CardTitle <CardTitle
title={intl.formatMessage({ title={title}
id: "BvmnJq",
defaultMessage: "Third Party Apps",
description: "section header",
})}
toolbar={ toolbar={
displayQuickManifestButton ? (
<InstallWithManifestFormButton <InstallWithManifestFormButton
onSubmitted={navigateToAppInstallPage} onSubmitted={navigateToAppInstallPage}
/> />
) : (
undefined
)
} }
/> />
<ResponsiveTable> <ResponsiveTable>
@ -85,15 +89,22 @@ const InstalledApps: React.FC<InstalledAppsProps> = ({
<span data-tc="name" className={classes.appName}> <span data-tc="name" className={classes.appName}>
{app.node.name} {app.node.name}
</span> </span>
{app.node.manifestUrl &&
isAppInTunnel(app.node.manifestUrl) ? (
<Typography variant="caption">
<FormattedMessage
defaultMessage="(TUNNEL - DEVELOPMENT)"
id="QdQ9z7"
/>
</Typography>
) : null}
</TableCell> </TableCell>
<TableCell className={classes.colAction}> <TableCell className={classes.colAction}>
{app.node.manifestUrl && ( {app.node.manifestUrl && (
<Typography <AppManifestTableDisplay
className={clsx(classes.text, classes.manifestUrl)} manifestUrl={app.node.manifestUrl}
variant="body2" />
>
{app.node.manifestUrl}
</Typography>
)} )}
<TableButtonWrapper> <TableButtonWrapper>
<Switch <Switch

View file

@ -0,0 +1,44 @@
import { SALEOR_APPS_ENDPOINT } from "@saleor/config";
import { useCallback, useState } from "react";
export interface SaleorApp {
name: string;
hostname: string;
}
const saleorAppsEnabled = Boolean(SALEOR_APPS_ENDPOINT);
export const useSaleorApps = () => {
const [apps, setApps] = useState<SaleorApp[] | undefined>(undefined);
const fetchApps = useCallback(async () => {
if (!saleorAppsEnabled) {
return;
}
return fetch(SALEOR_APPS_ENDPOINT)
.then(response => response.json())
.then((data: SaleorApp[]) => {
if (
!data.every(
item =>
typeof item.hostname === "string" ||
typeof item.name === "string",
)
) {
console.error(
'Invalid Saleor Apps data received from Marketplace. Expected array of objects with "name" and "hostname" properties',
);
return;
}
setApps(data);
});
}, []);
return {
saleorAppsEnabled,
apps,
fetchApps,
};
};

View file

@ -53,7 +53,7 @@ export const useStyles = makeStyles(
}, },
colName: { colName: {
paddingLeft: 0, paddingLeft: 0,
width: theme.spacing(30), minWidth: theme.spacing(30),
}, },
colSpinner: { colSpinner: {
"& svg": { "& svg": {
@ -213,9 +213,6 @@ export const useStyles = makeStyles(
marginRight: theme.spacing(1), marginRight: theme.spacing(1),
}, },
}, },
manifestUrl: {
marginRight: theme.spacing(1),
},
}), }),
{ name: "AppList" }, { name: "AppList" },
); );

4
src/apps/utils.ts Normal file
View file

@ -0,0 +1,4 @@
const tunnelKeywords = [".ngrok.io", ".saleor.live"];
export const isAppInTunnel = (manifestUrl: string) =>
Boolean(tunnelKeywords.find(keyword => manifestUrl.includes(keyword)));

View file

@ -9,6 +9,7 @@ export const API_URI = process.env.API_URI;
export const SW_INTERVAL = parseInt(process.env.SW_INTERVAL, 10); export const SW_INTERVAL = parseInt(process.env.SW_INTERVAL, 10);
export const IS_CLOUD_INSTANCE = process.env.IS_CLOUD_INSTANCE === "true"; export const IS_CLOUD_INSTANCE = process.env.IS_CLOUD_INSTANCE === "true";
export const MARKETPLACE_URL = process.env.MARKETPLACE_URL; export const MARKETPLACE_URL = process.env.MARKETPLACE_URL;
export const SALEOR_APPS_ENDPOINT = process.env.SALEOR_APPS_ENDPOINT;
export const DEFAULT_INITIAL_SEARCH_DATA: SearchVariables = { export const DEFAULT_INITIAL_SEARCH_DATA: SearchVariables = {
after: null, after: null,

View file

@ -25005,6 +25005,53 @@ exports[`Storyshots Views / Apps / Apps list default 1`] = `
/> />
</div> </div>
</div> </div>
<div
class="MuiTabs-root-id PageTab-containerRoot-id AppsListPageStyles-topTabs-id"
>
<div
class="MuiTabs-scroller-id MuiTabs-fixed-id"
style="overflow:hidden"
>
<div
class="MuiTabs-flexContainer-id PageTab-containerFlex-id"
role="tablist"
>
<button
aria-selected="true"
class="MuiButtonBase-root-id MuiTab-root-id PageTab-tabRoot-id MuiTab-textColorInherit-id MuiTab-selected-id"
role="tab"
tabindex="0"
type="button"
>
<span
class="MuiTab-wrapper-id"
>
3rd party apps
</span>
<span
class="PrivateTabIndicator-root-id PrivateTabIndicator-colorSecondary-id MuiTabs-indicator-id"
style="display:none"
/>
</button>
<button
aria-selected="false"
class="MuiButtonBase-root-id MuiTab-root-id PageTab-tabRoot-id MuiTab-textColorInherit-id"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper-id"
>
Webhooks & Events
</span>
</button>
</div>
</div>
</div>
<p>
Third party apps are installed with App Manifests. They contain UI accessible from dashboard and can extend it. Read more here.
</p>
<div <div
class="MuiPaper-root-id MuiCard-root-id AppList-apps-id MuiPaper-elevation0-id MuiPaper-rounded-id" class="MuiPaper-root-id MuiCard-root-id AppList-apps-id MuiPaper-elevation0-id MuiPaper-rounded-id"
> >
@ -25071,9 +25118,9 @@ exports[`Storyshots Views / Apps / Apps list default 1`] = `
class="MuiTableCell-root-id MuiTableCell-body-id AppList-colAction-id" class="MuiTableCell-root-id MuiTableCell-body-id AppList-colAction-id"
> >
<div <div
class="MuiTypography-root-id AppList-text-id AppList-manifestUrl-id MuiTypography-body2-id" class="MuiTypography-root-id AppManifestTableDisplay-manifestText-id MuiTypography-body1-id"
> >
http://localhost:3000/api/manifest localhost:3000
</div> </div>
<span <span
class="MuiSwitch-root-id" class="MuiSwitch-root-id"
@ -25168,9 +25215,9 @@ exports[`Storyshots Views / Apps / Apps list default 1`] = `
class="MuiTableCell-root-id MuiTableCell-body-id AppList-colAction-id" class="MuiTableCell-root-id MuiTableCell-body-id AppList-colAction-id"
> >
<div <div
class="MuiTypography-root-id AppList-text-id AppList-manifestUrl-id MuiTypography-body2-id" class="MuiTypography-root-id AppManifestTableDisplay-manifestText-id MuiTypography-body1-id"
> >
http://localhost:3000/api/manifest localhost:3000
</div> </div>
<span <span
class="MuiSwitch-root-id" class="MuiSwitch-root-id"
@ -25254,96 +25301,6 @@ exports[`Storyshots Views / Apps / Apps list default 1`] = `
<div <div
class="CardSpacer-spacer-id" class="CardSpacer-spacer-id"
/> />
<div
class="MuiPaper-root-id MuiCard-root-id AppList-customApps-id MuiPaper-elevation0-id MuiPaper-rounded-id"
>
<div
class="MuiCardHeader-root-id"
>
<div
class="MuiCardHeader-content-id"
>
<span
class="MuiTypography-root-id MuiCardHeader-title-id MuiTypography-h5-id MuiTypography-displayBlock-id"
>
Internal Apps
</span>
</div>
<div
class="MuiCardHeader-action-id"
>
<a
aria-disabled="false"
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id"
data-test-id="create-app"
href="/apps/custom/add"
role="button"
tabindex="0"
>
<span
class="MuiButton-label-id"
>
Create App
</span>
</a>
</div>
</div>
<div
class="ResponsiveTable-root-id"
>
<table
class="MuiTable-root-id"
>
<tbody
class="MuiTableBody-root-id"
>
<tr
class="MuiTableRow-root-id AppList-tableRow-id MuiTableRow-hover-id"
>
<td
class="MuiTableCell-root-id MuiTableCell-body-id AppList-colName-id"
>
<span
class="AppList-appName-id"
data-tc="name"
>
app custom
</span>
</td>
<td
class="MuiTableCell-root-id MuiTableCell-body-id AppList-colAction-id"
>
<button
class="MuiButtonBase-root-id IconButton-secondary-id IconButton-hoverOutline-id"
color="primary"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root-id MacawIcon-root-id"
fill="none"
focusable="false"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 10v8m3-8v8m3-8v8M5 4.5V21a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4.5m-14 0H4m1 0h4m10 0h1m-1 0h-4m-6 0V3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1.5m-6 0h6"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
</svg>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div <div
class="CardSpacer-spacer-id" class="CardSpacer-spacer-id"
/> />
@ -25559,6 +25516,53 @@ exports[`Storyshots Views / Apps / Apps list loading 1`] = `
/> />
</div> </div>
</div> </div>
<div
class="MuiTabs-root-id PageTab-containerRoot-id AppsListPageStyles-topTabs-id"
>
<div
class="MuiTabs-scroller-id MuiTabs-fixed-id"
style="overflow:hidden"
>
<div
class="MuiTabs-flexContainer-id PageTab-containerFlex-id"
role="tablist"
>
<button
aria-selected="true"
class="MuiButtonBase-root-id MuiTab-root-id PageTab-tabRoot-id MuiTab-textColorInherit-id MuiTab-selected-id"
role="tab"
tabindex="0"
type="button"
>
<span
class="MuiTab-wrapper-id"
>
3rd party apps
</span>
<span
class="PrivateTabIndicator-root-id PrivateTabIndicator-colorSecondary-id MuiTabs-indicator-id"
style="display:none"
/>
</button>
<button
aria-selected="false"
class="MuiButtonBase-root-id MuiTab-root-id PageTab-tabRoot-id MuiTab-textColorInherit-id"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper-id"
>
Webhooks & Events
</span>
</button>
</div>
</div>
</div>
<p>
Third party apps are installed with App Manifests. They contain UI accessible from dashboard and can extend it. Read more here.
</p>
<div <div
class="MuiPaper-root-id MuiCard-root-id AppList-apps-id MuiPaper-elevation0-id MuiPaper-rounded-id" class="MuiPaper-root-id MuiCard-root-id AppList-apps-id MuiPaper-elevation0-id MuiPaper-rounded-id"
> >
@ -25622,68 +25626,6 @@ exports[`Storyshots Views / Apps / Apps list loading 1`] = `
<div <div
class="CardSpacer-spacer-id" class="CardSpacer-spacer-id"
/> />
<div
class="MuiPaper-root-id MuiCard-root-id AppList-customApps-id MuiPaper-elevation0-id MuiPaper-rounded-id"
>
<div
class="MuiCardHeader-root-id"
>
<div
class="MuiCardHeader-content-id"
>
<span
class="MuiTypography-root-id MuiCardHeader-title-id MuiTypography-h5-id MuiTypography-displayBlock-id"
>
Internal Apps
</span>
</div>
<div
class="MuiCardHeader-action-id"
>
<a
aria-disabled="false"
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id"
data-test-id="create-app"
href="/apps/custom/add"
role="button"
tabindex="0"
>
<span
class="MuiButton-label-id"
>
Create App
</span>
</a>
</div>
</div>
<div
class="ResponsiveTable-root-id"
>
<table
class="MuiTable-root-id"
>
<tbody
class="MuiTableBody-root-id"
>
<tr
class="MuiTableRow-root-id AppList-tableRow-id MuiTableRow-hover-id"
>
<td
class="MuiTableCell-root-id MuiTableCell-body-id AppList-colName-id"
colspan="2"
>
<span
class="Skeleton-skeleton-id"
data-test-id="skeleton"
>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div> </div>
</div> </div>
`; `;
@ -25716,6 +25658,53 @@ exports[`Storyshots Views / Apps / Apps list no data 1`] = `
/> />
</div> </div>
</div> </div>
<div
class="MuiTabs-root-id PageTab-containerRoot-id AppsListPageStyles-topTabs-id"
>
<div
class="MuiTabs-scroller-id MuiTabs-fixed-id"
style="overflow:hidden"
>
<div
class="MuiTabs-flexContainer-id PageTab-containerFlex-id"
role="tablist"
>
<button
aria-selected="true"
class="MuiButtonBase-root-id MuiTab-root-id PageTab-tabRoot-id MuiTab-textColorInherit-id MuiTab-selected-id"
role="tab"
tabindex="0"
type="button"
>
<span
class="MuiTab-wrapper-id"
>
3rd party apps
</span>
<span
class="PrivateTabIndicator-root-id PrivateTabIndicator-colorSecondary-id MuiTabs-indicator-id"
style="display:none"
/>
</button>
<button
aria-selected="false"
class="MuiButtonBase-root-id MuiTab-root-id PageTab-tabRoot-id MuiTab-textColorInherit-id"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper-id"
>
Webhooks & Events
</span>
</button>
</div>
</div>
</div>
<p>
Third party apps are installed with App Manifests. They contain UI accessible from dashboard and can extend it. Read more here.
</p>
<div <div
class="MuiPaper-root-id MuiCard-root-id AppList-apps-id MuiPaper-elevation0-id MuiPaper-rounded-id" class="MuiPaper-root-id MuiCard-root-id AppList-apps-id MuiPaper-elevation0-id MuiPaper-rounded-id"
> >
@ -25777,66 +25766,6 @@ exports[`Storyshots Views / Apps / Apps list no data 1`] = `
<div <div
class="CardSpacer-spacer-id" class="CardSpacer-spacer-id"
/> />
<div
class="MuiPaper-root-id MuiCard-root-id AppList-customApps-id MuiPaper-elevation0-id MuiPaper-rounded-id"
>
<div
class="MuiCardHeader-root-id"
>
<div
class="MuiCardHeader-content-id"
>
<span
class="MuiTypography-root-id MuiCardHeader-title-id MuiTypography-h5-id MuiTypography-displayBlock-id"
>
Internal Apps
</span>
</div>
<div
class="MuiCardHeader-action-id"
>
<a
aria-disabled="false"
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-outlined-id MuiButton-outlinedPrimary-id"
data-test-id="create-app"
href="/apps/custom/add"
role="button"
tabindex="0"
>
<span
class="MuiButton-label-id"
>
Create App
</span>
</a>
</div>
</div>
<div
class="ResponsiveTable-root-id"
>
<table
class="MuiTable-root-id"
>
<tbody
class="MuiTableBody-root-id"
>
<tr
class="MuiTableRow-root-id AppList-tableRow-id MuiTableRow-hover-id"
>
<td
class="MuiTableCell-root-id MuiTableCell-body-id AppList-colName-id"
>
<div
class="MuiTypography-root-id AppList-text-id MuiTypography-body2-id"
>
Your custom-created apps will be shown here.
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div> </div>
</div> </div>
`; `;

View file

@ -39,6 +39,7 @@ const htmlWebpackPlugin = new HtmlWebpackPlugin({
const environmentPlugin = new webpack.EnvironmentPlugin({ const environmentPlugin = new webpack.EnvironmentPlugin({
API_URI: "", API_URI: "",
MARKETPLACE_URL: "", MARKETPLACE_URL: "",
SALEOR_APPS_ENDPOINT: "",
APP_MOUNT_URI: "/", APP_MOUNT_URI: "/",
DEMO_MODE: false, DEMO_MODE: false,
ENVIRONMENT: "", ENVIRONMENT: "",