Fix CMS app issues (#386)
* Fix CMS app visual issues * Add provider configuration instance ping status * Skip update webhooks processing if issuing principal is this CMS app * Fix provider configuration form validation * Create old-dingos-hide.md
This commit is contained in:
parent
2c0df91351
commit
a3636f73ef
21 changed files with 4658 additions and 4606 deletions
6
.changeset/old-dingos-hide.md
Normal file
6
.changeset/old-dingos-hide.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"saleor-app-cms": patch
|
||||
---
|
||||
|
||||
Fix CMS app issues
|
||||
Check if CMS provider instance configuration is working
|
|
@ -62,7 +62,7 @@
|
|||
"eslint-config-prettier": "^8.5.0",
|
||||
"jsdom": "^20.0.3",
|
||||
"prettier": "^2.7.1",
|
||||
"typescript": "4.9",
|
||||
"typescript": "5.0.4",
|
||||
"vitest": "^0.30.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,47 @@ import {
|
|||
ProductVariantUpdatedWebhookPayloadFragment,
|
||||
WebhookProductVariantFragment,
|
||||
} from "../../../../generated/graphql";
|
||||
import { CmsClientBatchOperations, CmsClientOperations, ProductResponseSuccess } from "../types";
|
||||
import {
|
||||
BaseResponse,
|
||||
CmsClientBatchOperations,
|
||||
CmsClientOperations,
|
||||
ProductResponseSuccess,
|
||||
} from "../types";
|
||||
import { getCmsIdFromSaleorItem } from "./metadata";
|
||||
import { logger as pinoLogger } from "../../logger";
|
||||
import { CMSProvider, cmsProviders } from "../providers";
|
||||
import { ProviderInstanceSchema, providersSchemaSet } from "../config";
|
||||
|
||||
export const pingProviderInstance = async (
|
||||
providerInstanceSettings: ProviderInstanceSchema
|
||||
): Promise<BaseResponse> => {
|
||||
const logger = pinoLogger.child({ providerInstanceSettings });
|
||||
logger.debug("Ping provider instance called");
|
||||
|
||||
const provider = cmsProviders[
|
||||
providerInstanceSettings.providerName as CMSProvider
|
||||
] as (typeof cmsProviders)[keyof typeof cmsProviders];
|
||||
|
||||
const validation =
|
||||
providersSchemaSet[providerInstanceSettings.providerName as CMSProvider].safeParse(
|
||||
providerInstanceSettings
|
||||
);
|
||||
|
||||
if (!validation.success) {
|
||||
logger.error("The provider instance settings validation failed.", {
|
||||
error: validation.error.message,
|
||||
});
|
||||
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
const config = validation.data;
|
||||
|
||||
const client = provider.create(config as any); // config without validation = providerInstanceSettings as any
|
||||
const pingResult = await client.ping();
|
||||
|
||||
return pingResult;
|
||||
};
|
||||
|
||||
interface CmsClientOperationResult {
|
||||
createdCmsId?: string;
|
||||
|
|
|
@ -31,7 +31,7 @@ export const providersConfig = {
|
|||
name: "token",
|
||||
label: "Token",
|
||||
helpText:
|
||||
'You can find this in your Contentful project, go to Settings > API keys > Content management tokens > Generate personal token. More instructions at [Contentful "Authentication" documentation](https://www.contentful.com/developers/docs/references/authentication/).',
|
||||
'You can find this in your Contentful project, go to Settings > API Keys > Content Management Tokens > Generate Personal Token. More instructions at [Contentful "Authentication" documentation](https://www.contentful.com/developers/docs/references/authentication/).',
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
|
@ -45,14 +45,14 @@ export const providersConfig = {
|
|||
name: "spaceId",
|
||||
label: "Space ID",
|
||||
helpText:
|
||||
"You can find this in your Contentful project, go to settings > general settings.",
|
||||
"You can find this in your Contentful project, go to Settings > General Settings.",
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
name: "contentId",
|
||||
label: "Content ID",
|
||||
helpText:
|
||||
"You can find this in your Contentful project, go to Content model > select model > Content type id.",
|
||||
"You can find this in your Contentful project, go to Content Model > select Model > Content Type ID.",
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
|
@ -71,7 +71,7 @@ export const providersConfig = {
|
|||
name: "apiRequestsPerSecond",
|
||||
label: "API requests per second",
|
||||
helpText:
|
||||
"API rate limits. Default 7. Used in bulk products variants sync. Higher rate limits may speed up a little products variants bulk sync. Higher rate limit may apply depending on different Contentful plan, learn more at https://www.contentful.com/developers/docs/references/content-management-api/#/introduction/api-rate-limits.",
|
||||
"API rate limits. The default is 7. Used in bulk products variants sync. Higher rate limits may speed up a little products variants bulk sync. Higher rate limit may apply depending on different Contentful plan, learn more at https://www.contentful.com/developers/docs/references/content-management-api/#/introduction/api-rate-limits.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -99,7 +99,7 @@ export const providersConfig = {
|
|||
name: "contentTypeId",
|
||||
label: "Content Type ID (plural)",
|
||||
helpText:
|
||||
'You can find this in your Strapi project, go to Content-Type Builder > select content type > click Edit > use API ID (Plural). More instructions at [Strapi "Editing content types" documentation](https://docs.strapi.io/user-docs/content-type-builder/managing-content-types#editing-content-types).',
|
||||
'You can find this in your Strapi project, go to Content-Type Builder > select Content Type > click Edit > Use API ID (Plural). More instructions at [Strapi "Editing content types" documentation](https://docs.strapi.io/user-docs/content-type-builder/managing-content-types#editing-content-types).',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -146,7 +146,7 @@ export type DatocmsConfig = CreateProviderConfig<"datocms">;
|
|||
export const strapiConfigSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
token: z.string().min(1),
|
||||
baseUrl: z.string().min(1),
|
||||
baseUrl: z.string().url().min(1),
|
||||
contentTypeId: z.string().min(1),
|
||||
});
|
||||
|
||||
|
@ -157,15 +157,15 @@ export const contentfulConfigSchema = z.object({
|
|||
spaceId: z.string().min(1),
|
||||
locale: z.string().min(1),
|
||||
contentId: z.string().min(1),
|
||||
baseUrl: z.string(),
|
||||
apiRequestsPerSecond: z.string(),
|
||||
baseUrl: z.string().url().optional().or(z.literal("")),
|
||||
apiRequestsPerSecond: z.number().optional().or(z.literal("")),
|
||||
});
|
||||
|
||||
export const datocmsConfigSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
token: z.string().min(1),
|
||||
itemTypeId: z.string().min(1),
|
||||
baseUrl: z.string(),
|
||||
itemTypeId: z.number().min(1),
|
||||
baseUrl: z.string().url().optional().or(z.literal("")),
|
||||
environment: z.string(),
|
||||
});
|
||||
|
||||
|
|
|
@ -101,6 +101,15 @@ const contentfulOperations: CreateOperations<ContentfulConfig> = (config) => {
|
|||
|
||||
const requestPerSecondLimit = Number(apiRequestsPerSecond || 7);
|
||||
|
||||
const pingCMS = async () => {
|
||||
const endpoint = `/spaces/${spaceId}`;
|
||||
const response = await contentfulFetch(endpoint, config, { method: "GET" });
|
||||
logger.debug({ response }, "pingCMS response");
|
||||
return {
|
||||
ok: response.ok,
|
||||
};
|
||||
};
|
||||
|
||||
const createProductInCMS = async (input: ProductInput): Promise<ContentfulResponse> => {
|
||||
// Contentful API does not auto generate resource ID during creation, it has to be provided.
|
||||
const resourceId = uuidv4();
|
||||
|
@ -203,6 +212,12 @@ const contentfulOperations: CreateOperations<ContentfulConfig> = (config) => {
|
|||
};
|
||||
|
||||
return {
|
||||
ping: async () => {
|
||||
const response = await pingCMS();
|
||||
logger.debug({ response }, "ping response");
|
||||
|
||||
return response;
|
||||
},
|
||||
createProduct: async ({ input }) => {
|
||||
const result = await createProductInCMS(input);
|
||||
logger.debug({ result }, "createProduct result");
|
||||
|
|
|
@ -50,10 +50,12 @@ const datocmsOperations: CreateOperations<DatocmsConfig> = (config) => {
|
|||
|
||||
const client = datocmsClient(config);
|
||||
|
||||
const pingCMS = async () => client.users.findMe();
|
||||
|
||||
const createProductInCMS = async (input: ProductInput) =>
|
||||
client.items.create({
|
||||
item_type: {
|
||||
id: config.itemTypeId,
|
||||
id: String(config.itemTypeId),
|
||||
type: "item_type",
|
||||
},
|
||||
saleor_id: input.saleorId,
|
||||
|
@ -91,6 +93,20 @@ const datocmsOperations: CreateOperations<DatocmsConfig> = (config) => {
|
|||
});
|
||||
|
||||
return {
|
||||
ping: async () => {
|
||||
try {
|
||||
const response = await pingCMS();
|
||||
logger.debug({ response }, "ping response");
|
||||
|
||||
if (!response.id) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
return { ok: false };
|
||||
}
|
||||
},
|
||||
createProduct: async ({ input }) => {
|
||||
try {
|
||||
const item = await createProductInCMS(input);
|
||||
|
|
|
@ -82,6 +82,14 @@ export const strapiOperations: CreateStrapiOperations = (config) => {
|
|||
|
||||
const { contentTypeId } = config;
|
||||
|
||||
const pingCMS = async () => {
|
||||
const response = await strapiFetch(`/${contentTypeId}`, config, {
|
||||
method: "GET",
|
||||
});
|
||||
logger.debug({ response }, "pingCMS response");
|
||||
return { ok: response.ok };
|
||||
};
|
||||
|
||||
const createProductInCMS = async (input: ProductInput): Promise<StrapiResponse> => {
|
||||
const body = transformInputToBody(input);
|
||||
const response = await strapiFetch(`/${contentTypeId}`, config, {
|
||||
|
@ -120,6 +128,12 @@ export const strapiOperations: CreateStrapiOperations = (config) => {
|
|||
};
|
||||
|
||||
return {
|
||||
ping: async () => {
|
||||
const response = await pingCMS();
|
||||
logger.debug({ response }, "ping response");
|
||||
|
||||
return response;
|
||||
},
|
||||
createProduct: async ({ input }) => {
|
||||
const result = await createProductInCMS(input);
|
||||
logger.debug({ result }, "createProduct result");
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { z } from "zod";
|
||||
import { providersConfig } from "./config";
|
||||
import { ProvidersSchema, providersConfig } from "./config";
|
||||
|
||||
export type ProductInput = Record<string, any> & {
|
||||
saleorId: string;
|
||||
|
@ -11,11 +11,13 @@ export type ProductInput = Record<string, any> & {
|
|||
image?: string;
|
||||
};
|
||||
|
||||
export type BaseResponse = { ok: boolean };
|
||||
export type ProductResponseSuccess = { ok: true; data: { id: string; saleorId: string } };
|
||||
export type ProductResponseError = { ok: false; error: string };
|
||||
export type ProductResponse = ProductResponseSuccess | ProductResponseError;
|
||||
|
||||
export type CmsOperations = {
|
||||
ping: () => Promise<BaseResponse>;
|
||||
getProduct?: ({ id }: { id: string }) => Promise<Response>;
|
||||
createProduct: ({ input }: { input: ProductInput }) => Promise<ProductResponse>;
|
||||
updateProduct: ({ id, input }: { id: string; input: ProductInput }) => Promise<Response | void>;
|
||||
|
@ -40,17 +42,14 @@ export type CmsClientBatchOperations = {
|
|||
operationType: keyof CmsBatchOperations;
|
||||
};
|
||||
|
||||
export type GetProviderTokens<TProviderName extends keyof typeof providersConfig> =
|
||||
(typeof providersConfig)[TProviderName]["tokens"][number];
|
||||
|
||||
export type BaseConfig = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
// * Generates the config based on the data supplied in the `providersConfig` variable.
|
||||
export type CreateProviderConfig<TProviderName extends keyof typeof providersConfig> = Record<
|
||||
GetProviderTokens<TProviderName>["name"],
|
||||
string
|
||||
export type CreateProviderConfig<TProviderName extends keyof typeof providersConfig> = Omit<
|
||||
ProvidersSchema[TProviderName],
|
||||
"id" | "providerName"
|
||||
> &
|
||||
BaseConfig;
|
||||
|
||||
|
|
|
@ -41,11 +41,15 @@ export const Channels = () => {
|
|||
}
|
||||
}, [channels]);
|
||||
|
||||
const handleOnSyncCompleted = (providerInstanceId: string) => {
|
||||
const handleOnSyncCompleted = (providerInstanceId: string, error?: string) => {
|
||||
if (!activeChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveChannel({
|
||||
...activeChannel,
|
||||
requireSyncProviderInstances: activeChannel.requireSyncProviderInstances?.filter(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { WebhookProductVariantFragment } from "../../../../generated/graphql";
|
||||
|
@ -21,9 +21,9 @@ interface UseProductsVariantsSyncHandlers {
|
|||
|
||||
export const useProductsVariantsSync = (
|
||||
channelSlug: string | null,
|
||||
onSyncCompleted: (providerInstanceId: string) => void
|
||||
onSyncCompleted: (providerInstanceId: string, error?: string) => void
|
||||
): UseProductsVariantsSyncHandlers => {
|
||||
const { appBridgeState } = useAppBridge();
|
||||
const { appBridge, appBridgeState } = useAppBridge();
|
||||
|
||||
const [startedProviderInstanceId, setStartedProviderInstanceId] = useState<string>();
|
||||
const [startedOperation, setStartedOperation] = useState<ProductsVariantsSyncOperation>();
|
||||
|
@ -78,6 +78,33 @@ export const useProductsVariantsSync = (
|
|||
}
|
||||
};
|
||||
|
||||
const completeSync = (completedProviderInstanceIdSync: string, error?: string) => {
|
||||
setStartedProviderInstanceId(undefined);
|
||||
setStartedOperation(undefined);
|
||||
setCurrentProductIndex(0);
|
||||
|
||||
onSyncCompleted(completedProviderInstanceIdSync, error);
|
||||
|
||||
if (error) {
|
||||
appBridge?.dispatch(
|
||||
actions.Notification({
|
||||
title: "Error",
|
||||
status: "error",
|
||||
text: "Error syncing products variants",
|
||||
apiMessage: error,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
appBridge?.dispatch(
|
||||
actions.Notification({
|
||||
title: "Success",
|
||||
status: "success",
|
||||
text: "Products variants sync completed successfully",
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
products.length <= currentProductIndex &&
|
||||
|
@ -85,13 +112,7 @@ export const useProductsVariantsSync = (
|
|||
startedProviderInstanceId &&
|
||||
startedOperation
|
||||
) {
|
||||
const completedProviderInstanceIdSync = startedProviderInstanceId;
|
||||
|
||||
setStartedProviderInstanceId(undefined);
|
||||
setStartedOperation(undefined);
|
||||
setCurrentProductIndex(0);
|
||||
|
||||
onSyncCompleted(completedProviderInstanceIdSync);
|
||||
completeSync(startedProviderInstanceId);
|
||||
}
|
||||
}, [products.length, currentProductIndex, fetchCompleted]);
|
||||
|
||||
|
@ -112,10 +133,19 @@ export const useProductsVariantsSync = (
|
|||
const productsBatch = products.slice(productsBatchStartIndex, productsBatchEndIndex);
|
||||
|
||||
// temporary solution, cannot use directly backend methods without fetch, due to non-browser Node dependency, like await cmsProvider.updatedBatchProducts(productsBatch);
|
||||
await syncFetch(startedProviderInstanceId, startedOperation, productsBatch);
|
||||
const syncResult = await syncFetch(
|
||||
startedProviderInstanceId,
|
||||
startedOperation,
|
||||
productsBatch
|
||||
);
|
||||
|
||||
if (syncResult.error) {
|
||||
completeSync(startedProviderInstanceId, syncResult.error);
|
||||
} else {
|
||||
setCurrentProductIndex(productsBatchEndIndex);
|
||||
}
|
||||
|
||||
setIsImporting(false);
|
||||
setCurrentProductIndex(productsBatchEndIndex);
|
||||
})();
|
||||
}, [
|
||||
startedProviderInstanceId,
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
|
||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
|
||||
const getCurrentTime = () => new Date().toLocaleTimeString();
|
||||
|
||||
export interface ProviderInstancePingStatus {
|
||||
providerInstanceId: string;
|
||||
success: boolean;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export interface PingProviderInstanceOpts {
|
||||
result: ProviderInstancePingStatus | null;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
export const usePingProviderInstance = (providerInstanceId: string | null) => {
|
||||
const { appBridgeState } = useAppBridge();
|
||||
|
||||
const [result, setResult] = useState<null | ProviderInstancePingStatus>(null);
|
||||
|
||||
const ping = async (providerInstanceId: string): Promise<ProviderInstancePingStatus> => {
|
||||
try {
|
||||
const pingResponse = await fetch("/api/ping-provider-instance", {
|
||||
method: "POST",
|
||||
headers: [
|
||||
["content-type", "application/json"],
|
||||
[SALEOR_API_URL_HEADER, appBridgeState?.saleorApiUrl!],
|
||||
[SALEOR_AUTHORIZATION_BEARER_HEADER, appBridgeState?.token!],
|
||||
],
|
||||
body: JSON.stringify({
|
||||
providerInstanceId,
|
||||
}),
|
||||
});
|
||||
|
||||
const pingResult = await pingResponse.json();
|
||||
|
||||
return {
|
||||
providerInstanceId,
|
||||
success: pingResult.success,
|
||||
time: getCurrentTime(),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("useProductsVariantsSync syncFetch error", error);
|
||||
|
||||
return {
|
||||
providerInstanceId,
|
||||
success: false,
|
||||
time: getCurrentTime(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
setResult(null);
|
||||
if (providerInstanceId) {
|
||||
ping(providerInstanceId).then((result) => setResult(result));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [providerInstanceId]);
|
||||
|
||||
return {
|
||||
result,
|
||||
refresh,
|
||||
};
|
||||
};
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from "../../../lib/cms/config";
|
||||
import { Provider } from "../../providers/config";
|
||||
import { AppMarkdownText } from "../../ui/app-markdown-text";
|
||||
import { ZodNumber } from "zod";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
footer: {
|
||||
|
@ -68,8 +69,6 @@ export const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderS
|
|||
}, [provider, providerInstance]);
|
||||
|
||||
const submitHandler = (values: SingleProviderSchema) => {
|
||||
console.log(values);
|
||||
|
||||
onSubmit(values);
|
||||
};
|
||||
|
||||
|
@ -111,35 +110,40 @@ export const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderS
|
|||
}
|
||||
/>
|
||||
</Grid>
|
||||
{fields.map((token) => (
|
||||
<Grid xs={12} item key={token.name}>
|
||||
<TextField
|
||||
{...register(token.name as Path<ProvidersSchema[TProvider]>, {
|
||||
required: "required" in token && token.required,
|
||||
})}
|
||||
// required={"required" in token && token.required}
|
||||
label={token.label}
|
||||
type={token.secret ? "password" : "text"}
|
||||
name={token.name}
|
||||
InputLabelProps={{
|
||||
shrink: !!watch(token.name as Path<ProvidersSchema[TProvider]>),
|
||||
}}
|
||||
fullWidth
|
||||
// @ts-ignore TODO: fix errors typing
|
||||
error={!!errors[token.name as Path<ProvidersSchema[TProvider]>]}
|
||||
helperText={
|
||||
<>
|
||||
{errors[token.name as Path<ProvidersSchema[TProvider]>]?.message ||
|
||||
("helpText" in token && (
|
||||
<AppMarkdownText>{`${getOptionalText(token)}${
|
||||
token.helpText
|
||||
}`}</AppMarkdownText>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
{fields.map((token) => {
|
||||
const isSecret = token.secret ? { type: "password" } : {};
|
||||
|
||||
return (
|
||||
<Grid xs={12} item key={token.name}>
|
||||
<TextField
|
||||
{...register(token.name as Path<ProvidersSchema[TProvider]>, {
|
||||
required: "required" in token && token.required,
|
||||
valueAsNumber:
|
||||
schema.shape[token.name as keyof typeof schema.shape] instanceof ZodNumber,
|
||||
})}
|
||||
{...isSecret}
|
||||
label={token.label}
|
||||
name={token.name}
|
||||
InputLabelProps={{
|
||||
shrink: !!watch(token.name as Path<ProvidersSchema[TProvider]>),
|
||||
}}
|
||||
fullWidth
|
||||
// @ts-ignore TODO: fix errors typing
|
||||
error={!!errors[token.name as Path<ProvidersSchema[TProvider]>]}
|
||||
helperText={
|
||||
<>
|
||||
{errors[token.name as Path<ProvidersSchema[TProvider]>]?.message ||
|
||||
("helpText" in token && (
|
||||
<AppMarkdownText>{`${getOptionalText(token)}${
|
||||
token.helpText
|
||||
}`}</AppMarkdownText>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
{providerInstance ? (
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
|
|
|
@ -8,6 +8,8 @@ import { ProviderInstanceConfigurationForm } from "./provider-instance-configura
|
|||
import { Skeleton } from "@material-ui/lab";
|
||||
import { ProvidersErrors, ProvidersLoading } from "./types";
|
||||
import { getProviderByName, Provider } from "../../providers/config";
|
||||
import { ProviderInstancePingStatus } from "./hooks/usePingProviderInstance";
|
||||
import { ProviderInstancePing } from "./provider-instance-ping";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
radioLabel: {
|
||||
|
@ -51,6 +53,12 @@ const useStyles = makeStyles((theme) => ({
|
|||
borderRadius: 8,
|
||||
padding: 20,
|
||||
},
|
||||
successStatus: {
|
||||
color: theme.palette.type === "dark" ? theme.palette.success.light : theme.palette.success.dark,
|
||||
},
|
||||
errorStatus: {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
}));
|
||||
|
||||
const ProviderInstanceConfigurationSkeleton = () => {
|
||||
|
@ -94,6 +102,7 @@ interface ProviderInstanceConfigurationProps {
|
|||
deleteProviderInstance: (providerInstance: SingleProviderSchema) => any;
|
||||
loading: ProvidersLoading;
|
||||
errors: ProvidersErrors;
|
||||
providerInstancePingStatus: ProviderInstancePingStatus | null;
|
||||
onNewProviderRequest(): void;
|
||||
}
|
||||
|
||||
|
@ -103,6 +112,7 @@ export const ProviderInstanceConfiguration = ({
|
|||
saveProviderInstance,
|
||||
deleteProviderInstance,
|
||||
loading,
|
||||
providerInstancePingStatus,
|
||||
onNewProviderRequest,
|
||||
errors,
|
||||
}: ProviderInstanceConfigurationProps) => {
|
||||
|
@ -124,6 +134,9 @@ export const ProviderInstanceConfiguration = ({
|
|||
setSelectedProvider(provider);
|
||||
};
|
||||
|
||||
const isPingStatusLoading =
|
||||
providerInstancePingStatus?.providerInstanceId !== activeProviderInstance?.id;
|
||||
|
||||
if (loading.fetching || loading.saving) {
|
||||
return <ProviderInstanceConfigurationSkeleton />;
|
||||
}
|
||||
|
@ -196,6 +209,15 @@ export const ProviderInstanceConfiguration = ({
|
|||
</RadioGroup>
|
||||
{selectedProvider ? (
|
||||
<>
|
||||
{!newProviderInstance && (
|
||||
<>
|
||||
<br />
|
||||
<ProviderInstancePing
|
||||
loading={isPingStatusLoading}
|
||||
status={providerInstancePingStatus}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
<ProviderInstanceConfigurationForm
|
||||
provider={selectedProvider}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import { Skeleton } from "@material-ui/lab";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { ProviderInstancePingStatus } from "./hooks/usePingProviderInstance";
|
||||
import clsx from "clsx";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
successStatus: {
|
||||
color: theme.palette.type === "dark" ? theme.palette.success.light : theme.palette.success.dark,
|
||||
},
|
||||
errorStatus: {
|
||||
color: theme.palette.error.main,
|
||||
},
|
||||
}));
|
||||
|
||||
interface ProviderInstancePingStatusProps {
|
||||
loading: boolean;
|
||||
status: ProviderInstancePingStatus | null;
|
||||
}
|
||||
|
||||
export const ProviderInstancePing = ({ loading, status }: ProviderInstancePingStatusProps) => {
|
||||
const styles = useStyles();
|
||||
|
||||
const parseProviderInstanceStatus = () => {
|
||||
const statusText = status?.success ? "Ok" : "Error";
|
||||
const checkTime = `(check time ${status?.time})`;
|
||||
|
||||
return `Configuration connection: ${statusText} ${checkTime}`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography
|
||||
variant="body1"
|
||||
className={clsx({
|
||||
[styles.successStatus]: status?.success,
|
||||
[styles.errorStatus]: !status?.success,
|
||||
})}
|
||||
>
|
||||
{parseProviderInstanceStatus()}
|
||||
</Typography>
|
||||
);
|
||||
};
|
|
@ -8,6 +8,7 @@ import { Button, makeStyles } from "@saleor/macaw-ui";
|
|||
import { ProviderInstancesSelect } from "./provider-instances-list";
|
||||
import { Add } from "@material-ui/icons";
|
||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||
import { usePingProviderInstance } from "./hooks/usePingProviderInstance";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
wrapper: {
|
||||
|
@ -26,6 +27,7 @@ export const ProviderInstances = () => {
|
|||
const [newProviderInstance, setNewProviderInstance] = useState<SingleProviderSchema | null>(null);
|
||||
|
||||
const { notifySuccess } = useDashboardNotification();
|
||||
const pingProviderInstanceOpts = usePingProviderInstance(activeProviderInstanceId);
|
||||
|
||||
useEffect(() => {
|
||||
if (providerInstances.length && !activeProviderInstanceId) {
|
||||
|
@ -58,6 +60,9 @@ export const ProviderInstances = () => {
|
|||
if (newProviderInstance && savedProviderInstance) {
|
||||
setActiveProviderInstanceId(savedProviderInstance.id);
|
||||
}
|
||||
if (!newProviderInstance) {
|
||||
pingProviderInstanceOpts.refresh();
|
||||
}
|
||||
};
|
||||
const handleDeleteProviderInstance = async (providerInstance: SingleProviderSchema) => {
|
||||
await deleteProviderInstance(providerInstance);
|
||||
|
@ -94,6 +99,7 @@ export const ProviderInstances = () => {
|
|||
deleteProviderInstance={handleDeleteProviderInstance}
|
||||
loading={loading}
|
||||
errors={errors}
|
||||
providerInstancePingStatus={pingProviderInstanceOpts.result}
|
||||
onNewProviderRequest={handleAddNewProviderInstance}
|
||||
/>
|
||||
</div>
|
||||
|
|
70
apps/cms/src/pages/api/ping-provider-instance.ts
Normal file
70
apps/cms/src/pages/api/ping-provider-instance.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { NextProtectedApiHandler, createProtectedHandler } from "@saleor/app-sdk/handlers/next";
|
||||
import { saleorApp } from "../../../saleor-app";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { logger as pinoLogger } from "../../lib/logger";
|
||||
import { createClient } from "../../lib/graphql";
|
||||
import { createSettingsManager } from "../../lib/metadata";
|
||||
import { getProviderInstancesSettings } from "../../lib/cms/client/settings";
|
||||
import { pingProviderInstance } from "../../lib/cms/client/clients-execution";
|
||||
|
||||
export interface ProviderInstancePingApiPayload {
|
||||
providerInstanceId: string;
|
||||
}
|
||||
|
||||
export interface ProviderInstancePingApiResponse {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
const handler: NextProtectedApiHandler = async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<ProviderInstancePingApiResponse>,
|
||||
context
|
||||
) => {
|
||||
const { authData } = context;
|
||||
const { providerInstanceId } = req.body as ProviderInstancePingApiPayload;
|
||||
|
||||
const logger = pinoLogger.child({
|
||||
endpoint: "ping-provider-instance",
|
||||
});
|
||||
logger.debug({ providerInstanceId }, "Called endpoint ping-provider-instance");
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (!providerInstanceId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
|
||||
const client = createClient(authData.saleorApiUrl, async () => ({
|
||||
token: authData.token,
|
||||
}));
|
||||
const settingsManager = createSettingsManager(client);
|
||||
const providerInstancesSettingsParsed = await getProviderInstancesSettings(settingsManager);
|
||||
|
||||
const providerInstanceSettings = providerInstancesSettingsParsed[providerInstanceId];
|
||||
|
||||
if (!providerInstanceSettings) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
|
||||
const pingResult = await pingProviderInstance(providerInstanceSettings);
|
||||
|
||||
if (!pingResult.ok) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
});
|
||||
};
|
||||
|
||||
export default createProtectedHandler(handler, saleorApp.apl, ["MANAGE_APPS"]);
|
|
@ -156,13 +156,25 @@ const handler: NextProtectedApiHandler = async (
|
|||
})) || [],
|
||||
});
|
||||
|
||||
if (syncResult?.error) {
|
||||
logger.error({ error: syncResult.error }, "The sync result error.");
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
data: {
|
||||
createdCMSIds: syncResult?.createdCmsIds || [],
|
||||
deletedCMSIds: syncResult?.deletedCmsIds || [],
|
||||
},
|
||||
error: syncResult?.error,
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug("The sync result success.");
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
createdCMSIds: syncResult?.createdCmsIds || [],
|
||||
deletedCMSIds: syncResult?.deletedCmsIds || [],
|
||||
},
|
||||
error: syncResult?.error,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
12
apps/cms/src/pages/api/webhooks/_utils.ts
Normal file
12
apps/cms/src/pages/api/webhooks/_utils.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
interface UnknownIssuingPrincipal {
|
||||
__typename?: string;
|
||||
}
|
||||
|
||||
export const isAppWebhookIssuer = <T extends UnknownIssuingPrincipal | null | undefined>(
|
||||
issuingPrincipal: T,
|
||||
appId: string
|
||||
) =>
|
||||
issuingPrincipal &&
|
||||
(!issuingPrincipal.__typename || issuingPrincipal.__typename === "App") &&
|
||||
"id" in issuingPrincipal &&
|
||||
issuingPrincipal.id === appId;
|
|
@ -11,6 +11,7 @@ import { createCmsOperations, executeCmsOperations, updateMetadata } from "../..
|
|||
import { logger as pinoLogger } from "../../../lib/logger";
|
||||
import { createClient } from "../../../lib/graphql";
|
||||
import { fetchProductVariantMetadata } from "../../../lib/metadata";
|
||||
import { isAppWebhookIssuer } from "./_utils";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
|
@ -21,6 +22,11 @@ export const config = {
|
|||
export const ProductUpdatedWebhookPayload = gql`
|
||||
${UntypedWebhookProductFragmentDoc}
|
||||
fragment ProductUpdatedWebhookPayload on ProductUpdated {
|
||||
issuingPrincipal {
|
||||
... on App {
|
||||
id
|
||||
}
|
||||
}
|
||||
product {
|
||||
...WebhookProduct
|
||||
}
|
||||
|
@ -49,13 +55,19 @@ export const handler: NextWebhookApiHandler<ProductUpdatedWebhookPayloadFragment
|
|||
res,
|
||||
context
|
||||
) => {
|
||||
const { product } = context.payload;
|
||||
const { saleorApiUrl, token } = context.authData;
|
||||
const { product, issuingPrincipal } = context.payload;
|
||||
const { saleorApiUrl, token, appId } = context.authData;
|
||||
|
||||
const logger = pinoLogger.child({
|
||||
product,
|
||||
});
|
||||
logger.debug("Called webhook PRODUCT_UPDATED");
|
||||
logger.debug({ issuingPrincipal }, "Issuing principal");
|
||||
|
||||
if (isAppWebhookIssuer(issuingPrincipal, appId)) {
|
||||
logger.debug("Issuing principal is the same as the app. Skipping webhook processing.");
|
||||
return res.status(200).end();
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
return res.status(500).json({
|
||||
|
|
|
@ -11,6 +11,7 @@ import { createCmsOperations, executeCmsOperations, updateMetadata } from "../..
|
|||
import { logger as pinoLogger } from "../../../lib/logger";
|
||||
import { createClient } from "../../../lib/graphql";
|
||||
import { fetchProductVariantMetadata } from "../../../lib/metadata";
|
||||
import { isAppWebhookIssuer } from "./_utils";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
|
@ -21,6 +22,11 @@ export const config = {
|
|||
export const ProductVariantUpdatedWebhookPayload = gql`
|
||||
${UntypedWebhookProductVariantFragmentDoc}
|
||||
fragment ProductVariantUpdatedWebhookPayload on ProductVariantUpdated {
|
||||
issuingPrincipal {
|
||||
... on App {
|
||||
id
|
||||
}
|
||||
}
|
||||
productVariant {
|
||||
...WebhookProductVariant
|
||||
}
|
||||
|
@ -50,13 +56,19 @@ export const handler: NextWebhookApiHandler<ProductVariantUpdatedWebhookPayloadF
|
|||
res,
|
||||
context
|
||||
) => {
|
||||
const { productVariant } = context.payload;
|
||||
const { saleorApiUrl, token } = context.authData;
|
||||
const { productVariant, issuingPrincipal } = context.payload;
|
||||
const { saleorApiUrl, token, appId } = context.authData;
|
||||
|
||||
const logger = pinoLogger.child({
|
||||
productVariant,
|
||||
});
|
||||
logger.debug("Called webhook PRODUCT_VARIANT_UPDATED");
|
||||
logger.debug({ issuingPrincipal }, "Issuing principal");
|
||||
|
||||
if (isAppWebhookIssuer(issuingPrincipal, appId)) {
|
||||
logger.debug("Issuing principal is the same as the app. Skipping webhook processing.");
|
||||
return res.status(200).end();
|
||||
}
|
||||
|
||||
if (!productVariant) {
|
||||
return res.status(500).json({
|
||||
|
|
8738
pnpm-lock.yaml
8738
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue