Monitoring app UI (#677)

* Replace macaw to next

* UI WIP

* ui wip

* fix graphql calls

* Fix ui

* Changsets

* Apply CR review

* fix spacing
This commit is contained in:
Lukasz Ostrowski 2023-06-27 19:20:58 +02:00 committed by GitHub
parent f6ff907cfd
commit 3bd7e3f05d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 267 additions and 526 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-monitoring": major
---
Updated App's UI to the new Macaw. Simplified the view and removed unnecessary not-implemented providers.

View file

@ -0,0 +1 @@
MONITORING_APP_API_URL=

View file

@ -55,6 +55,8 @@ To use Graphql Playground, `Monitoring` app needs to be installed in Saleor, and
### Testing DataDog integration ### Testing DataDog integration
Set `MOCK_DATADOG_CLIENT` env to `True`
Use these credentials sets to test DataDog integration: Use these credentials sets to test DataDog integration:
Working credentials: Working credentials:

View file

@ -12,5 +12,7 @@ services:
- "5001:80" - "5001:80"
environment: environment:
- DEBUG=True - DEBUG=True
# Uncomment to enable test credentials mode
# - MOCK_DATADOG_CLIENT=True
volumes: volumes:
- ./backend/monitoring/:/app/monitoring - ./backend/monitoring/:/app/monitoring

View file

@ -1,14 +1,18 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
module.exports = { module.exports = {
transpilePackages: ["@saleor/apps-shared"],
reactStrictMode: true, reactStrictMode: true,
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
rewrites() { rewrites() {
/** /**
* For dev/preview Next.js can work as a proxy and redirect unknown paths to provided backend address * For dev/preview Next.js can work as a proxy and redirect unknown paths to provided backend address
* *
* In production, when env is not provided, frontend will call its relative path and reverse proxy will do the rest * In production, when env is not provided, frontend will call its relative path and reverse proxy will do the rest
*/ */
const backendPath = process.env.MONITORING_APP_API_URL ?? ""; const backendPath = process.env.MONITORING_APP_API_URL;
if(!backendPath) {
throw new Error('Please set MONITORING_APP_API_URL variable')
}
return { return {
fallback: [ fallback: [

View file

@ -16,7 +16,9 @@
"@material-ui/lab": "4.0.0-alpha.61", "@material-ui/lab": "4.0.0-alpha.61",
"@saleor/app-sdk": "0.40.1", "@saleor/app-sdk": "0.40.1",
"@saleor/apps-shared": "workspace:*", "@saleor/apps-shared": "workspace:*",
"@saleor/macaw-ui": "^0.7.2", "@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.95",
"@saleor/react-hook-form-macaw": "workspace:*",
"@urql/exchange-auth": "^2.1.4", "@urql/exchange-auth": "^2.1.4",
"@vitejs/plugin-react": "4.0.0", "@vitejs/plugin-react": "4.0.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",
@ -28,7 +30,7 @@
"pino-pretty": "^10.0.0", "pino-pretty": "^10.0.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.42.1", "react-hook-form": "^7.43.9",
"urql": "^4.0.4", "urql": "^4.0.4",
"vite": "4.3.9", "vite": "4.3.9",
"vitest": "0.31.3" "vitest": "0.31.3"
@ -50,8 +52,5 @@
"eslint-config-saleor": "workspace:*", "eslint-config-saleor": "workspace:*",
"typescript": "5.1.3" "typescript": "5.1.3"
}, },
"private": true, "private": true
"saleor": {
"schemaVersion": "3.10"
}
} }

View file

@ -9,9 +9,9 @@ export const API_KEYS_LINKS: { [key in DatadogSite]: string } = {
}; };
export const DATADOG_SITES_LINKS: { [key in DatadogSite]: string } = { export const DATADOG_SITES_LINKS: { [key in DatadogSite]: string } = {
[DatadogSite.Us1]: "datadoghq.com", [DatadogSite.Us1]: "https://datadoghq.com",
[DatadogSite.Us3]: "us3.datadoghq.com", [DatadogSite.Us3]: "https://us3.datadoghq.com",
[DatadogSite.Us5]: "us5.datadoghq.com", [DatadogSite.Us5]: "https://us5.datadoghq.com",
[DatadogSite.Eu1]: "datadoghq.eu", [DatadogSite.Eu1]: "https://datadoghq.eu",
[DatadogSite.Us1Fed]: "ddog-gov.com", [DatadogSite.Us1Fed]: "https://ddog-gov.com",
}; };

View file

@ -1,7 +1,49 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge"; import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { createGraphQLClient } from "@saleor/apps-shared";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
import { Provider } from "urql"; import { cacheExchange, createClient as urqlCreateClient, fetchExchange, Provider } from "urql";
import { authExchange } from "@urql/exchange-auth";
/**
* Local client creation. Contrary to other apps, Monitoring frontend doesnt contact Saleor directly,
* but calls Python-based service which also provides graphQL endpoint.
*
* App calls /graphql/ which is rewritten MONITORING_APP_API_URL. See next.config.js
*/
const createGraphQLClient = ({
graphql,
saleorApiUrl,
token,
}: {
graphql: string;
saleorApiUrl: string;
token: string;
}) => {
return urqlCreateClient({
url: graphql,
exchanges: [
cacheExchange,
authExchange(async (utils) => {
return {
addAuthToOperation(operation) {
const headers: Record<string, string> = token
? {
"Authorization-Bearer": token,
"Saleor-Api-Url": saleorApiUrl,
}
: {};
return utils.appendHeaders(operation, headers);
},
didAuthError(error) {
return error.graphQLErrors.some((e) => e.extensions?.code === "FORBIDDEN");
},
async refreshAuth() {},
};
}),
fetchExchange,
],
});
};
export function GraphQLProvider(props: PropsWithChildren<{}>) { export function GraphQLProvider(props: PropsWithChildren<{}>) {
const { appBridgeState } = useAppBridge(); const { appBridgeState } = useAppBridge();
@ -11,6 +53,7 @@ export function GraphQLProvider(props: PropsWithChildren<{}>) {
const client = createGraphQLClient({ const client = createGraphQLClient({
saleorApiUrl, saleorApiUrl,
token, token,
graphql: "/graphql/",
}); });
return <Provider value={client} {...props} />; return <Provider value={client} {...props} />;

View file

@ -1,7 +0,0 @@
export function isInIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}

View file

@ -1,19 +0,0 @@
import React, { PropsWithChildren } from "react";
import dynamic from "next/dynamic";
const Wrapper = (props: PropsWithChildren<{}>) => <React.Fragment>{props.children}</React.Fragment>;
/**
* Saleor App can be rendered only as a Saleor Dashboard iframe.
* All content is rendered after Dashboard exchanges auth with the app.
* Hence, there is no reason to render app server side.
*
* This component forces app to work in SPA-mode. It simplifies browser-only code and reduces need
* of using dynamic() calls
*
* You can use this wrapper selectively for some pages or remove it completely.
* It doesn't affect Saleor communication, but may cause problems with some client-only code.
*/
export const NoSSRWrapper = dynamic(() => Promise.resolve(Wrapper), {
ssr: false,
});

View file

@ -1,48 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { AppBridgeState } from "@saleor/app-sdk/app-bridge";
import { render, waitFor } from "@testing-library/react";
import { ThemeSynchronizer } from "./theme-synchronizer";
const appBridgeState: AppBridgeState = {
ready: true,
token: "token",
domain: "some-domain.saleor.cloud",
theme: "dark",
path: "/",
locale: "en",
id: "app-id",
saleorApiUrl: "https://some-domain.saleor.cloud/graphql/",
};
const mockThemeChange = vi.fn();
vi.mock("@saleor/app-sdk/app-bridge", () => {
return {
useAppBridge() {
return {
appBridgeState: appBridgeState,
};
},
};
});
vi.mock("@saleor/macaw-ui", () => {
return {
useTheme() {
return {
setTheme: mockThemeChange,
themeType: "light",
};
},
};
});
describe("ThemeSynchronizer", () => {
it("Updates MacawUI theme when AppBridgeState theme changes", () => {
render(<ThemeSynchronizer />);
return waitFor(() => {
expect(mockThemeChange).toHaveBeenCalledWith("dark");
});
});
});

View file

@ -1,33 +1,25 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge"; import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useTheme } from "@saleor/macaw-ui"; import { useTheme } from "@saleor/macaw-ui/next";
import { memo, useEffect } from "react"; import { useEffect } from "react";
/** // todo move to shared
* Macaw-ui stores its theme mode in memory and local storage. To synchronize App with Dashboard, export function ThemeSynchronizer() {
* Macaw must be informed about this change from AppBridge.
*
* If you are not using Macaw, you can remove this.
*/
function _ThemeSynchronizer() {
const { appBridgeState } = useAppBridge(); const { appBridgeState } = useAppBridge();
const { setTheme, themeType } = useTheme(); const { setTheme } = useTheme();
useEffect(() => { useEffect(() => {
if (!setTheme || !appBridgeState?.theme) { if (!setTheme || !appBridgeState?.theme) {
return; return;
} }
if (themeType !== appBridgeState?.theme) { if (appBridgeState.theme === "light") {
setTheme(appBridgeState.theme); setTheme("defaultLight");
/**
* Hack to fix macaw, which is going into infinite loop on light mode (probably de-sync local storage with react state)
* TODO Fix me when Macaw 2.0 is shipped
*/
window.localStorage.setItem("macaw-ui-theme", appBridgeState.theme);
} }
}, [appBridgeState?.theme, setTheme, themeType]);
if (appBridgeState.theme === "dark") {
setTheme("defaultDark");
}
}, [appBridgeState?.theme, setTheme]);
return null; return null;
} }
export const ThemeSynchronizer = memo(_ThemeSynchronizer);

View file

@ -1,88 +1,30 @@
import "../styles/globals.css"; import "@saleor/macaw-ui/next/style";
import "../style.css";
import { Theme } from "@material-ui/core/styles";
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge"; import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next"; import React from "react";
import {
dark,
light,
SaleorThemeColors,
ThemeProvider as MacawUIThemeProvider,
} from "@saleor/macaw-ui";
import React, { PropsWithChildren, useEffect } from "react";
import { AppProps } from "next/app"; import { AppProps } from "next/app";
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
import { ThemeSynchronizer } from "../lib/theme-synchronizer"; import { ThemeSynchronizer } from "../lib/theme-synchronizer";
import { NoSSRWrapper } from "../lib/no-ssr-wrapper"; import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
import { NoSSRWrapper } from "@saleor/apps-shared";
import { GraphQLProvider } from "../graphql-provider"; import { GraphQLProvider } from "../graphql-provider";
const themeOverrides: Partial<Theme> = {
/**
* You can override MacawUI theme here
*/
};
type PalettesOverride = Record<"light" | "dark", SaleorThemeColors>;
/**
* Temporary override of colors, to match new dashboard palette.
* Long term this will be replaced with Macaw UI 2.x with up to date design tokens
*/
const palettes: PalettesOverride = {
light: {
...light,
background: {
default: "#fff",
paper: "#fff",
},
},
dark: {
...dark,
background: {
default: "hsla(211, 42%, 14%, 1)",
paper: "hsla(211, 42%, 14%, 1)",
},
},
};
/** /**
* Ensure instance is a singleton. * Ensure instance is a singleton.
* TODO: This is React 18 issue, consider hiding this workaround inside app-sdk
*/ */
const appBridgeInstance = export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
typeof window !== "undefined"
? new AppBridge({
initialTheme: "light",
})
: undefined;
/**
* That's a hack required by Macaw-UI incompatibility with React@18
*/
const ThemeProvider = MacawUIThemeProvider as React.FC<
PropsWithChildren<{ overrides?: Partial<Theme>; ssr: boolean; palettes: PalettesOverride }>
>;
function NextApp({ Component, pageProps }: AppProps) { function NextApp({ Component, pageProps }: AppProps) {
/**
* Configure JSS (used by MacawUI) for SSR. If Macaw is not used, can be removed.
*/
useEffect(() => {
const jssStyles = document.querySelector("#jss-server-side");
if (jssStyles) {
jssStyles?.parentElement?.removeChild(jssStyles);
}
}, []);
return ( return (
<NoSSRWrapper> <NoSSRWrapper>
<AppBridgeProvider appBridgeInstance={appBridgeInstance}> <AppBridgeProvider appBridgeInstance={appBridgeInstance}>
<GraphQLProvider> <GraphQLProvider>
<ThemeProvider overrides={themeOverrides} ssr palettes={palettes}> <ThemeProvider>
<ThemeSynchronizer /> <ThemeSynchronizer />
<RoutePropagator /> <RoutePropagator />
<Box padding={4}>
<Component {...pageProps} /> <Component {...pageProps} />
</Box>
</ThemeProvider> </ThemeProvider>
</GraphQLProvider> </GraphQLProvider>
</AppBridgeProvider> </AppBridgeProvider>

View file

@ -1,16 +1,13 @@
import { NextPage } from "next"; import { NextPage } from "next";
import { AppColumnsLayout } from "../../ui/app-columns-layout";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { IntegrationsList } from "../../ui/providers-list";
import { NoProvidersConfigured } from "../../ui/no-providers-configured"; import { NoProvidersConfigured } from "../../ui/no-providers-configured";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { DatadogConfig } from "../../ui/datadog/datadog-config"; import { DatadogConfig } from "../../ui/datadog/datadog-config";
import { DatadogSite, useConfigQuery } from "../../../generated/graphql"; import { DatadogSite, useConfigQuery } from "../../../generated/graphql";
import { LinearProgress, Link, Typography } from "@material-ui/core"; import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { Section } from "../../ui/sections";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
import { Done, Error } from "@material-ui/icons";
import { DATADOG_SITES_LINKS } from "../../datadog-urls"; import { DATADOG_SITES_LINKS } from "../../datadog-urls";
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import { Breadcrumbs, TextLink } from "@saleor/apps-ui";
const useActiveProvider = () => { const useActiveProvider = () => {
const router = useRouter(); const router = useRouter();
@ -20,13 +17,15 @@ const useActiveProvider = () => {
return selectedProvider ?? null; return selectedProvider ?? null;
}; };
const Content = () => { const ConfigurationPage = () => {
const [configuration, fetchConfiguration] = useConfigQuery(); const [configuration, fetchConfiguration] = useConfigQuery();
const { appBridge } = useAppBridge(); const { appBridge } = useAppBridge();
const datadogCredentials = configuration.data?.integrations.datadog?.credentials; const datadogCredentials = configuration.data?.integrations.datadog?.credentials;
const datadogError = configuration.data?.integrations.datadog?.error; const datadogError = configuration.data?.integrations.datadog?.error;
const { push } = useRouter();
useEffect(() => { useEffect(() => {
fetchConfiguration(); fetchConfiguration();
}, [fetchConfiguration]); }, [fetchConfiguration]);
@ -34,7 +33,7 @@ const Content = () => {
const selectedProvider = useActiveProvider(); const selectedProvider = useActiveProvider();
if (configuration.fetching && !configuration.data) { if (configuration.fetching && !configuration.data) {
return <LinearProgress />; return <Text>Loading...</Text>;
} }
if (selectedProvider === "datadog") { if (selectedProvider === "datadog") {
@ -50,57 +49,48 @@ const Content = () => {
const site = configuration.data?.integrations.datadog?.credentials.site ?? DatadogSite.Us1; const site = configuration.data?.integrations.datadog?.credentials.site ?? DatadogSite.Us1;
return ( return (
<Section> <Box display={"flex"} gap={4} flexDirection={"column"}>
<Typography paragraph variant="h3"> <Text as={"h1"} variant="heading">
<Done style={{ verticalAlign: "middle", marginRight: 10 }} />
App configured App configured
</Typography> </Text>
<Typography paragraph> <Text as={"p"}>
Visit{" "} Visit{" "}
<Link <TextLink newTab href={DATADOG_SITES_LINKS[site] ?? "https://app.datadoghq.com/"}>
href="https://app.datadoghq.com/" Datadog
onClick={(e) => { </TextLink>{" "}
e.preventDefault(); to access your logs
appBridge?.dispatch( </Text>
actions.Redirect({ <Button
to: DATADOG_SITES_LINKS[site], onClick={() => {
newContext: true, push("/configuration/datadog");
})
);
}} }}
> >
Datadog Edit configuration
</Link>{" "} </Button>
to access your logs </Box>
</Typography>
</Section>
); );
} }
if (datadogError) { if (datadogError) {
return ( return (
<Section> <Box>
<Typography paragraph variant="h3"> <Text variant="heading" as={"h1"}>
<Error style={{ verticalAlign: "middle", marginRight: 10 }} />
Configuration Error Configuration Error
</Typography> </Text>
<Typography>{datadogError}</Typography> <Text color={"textCriticalDefault"}>{datadogError}</Text>
</Section> <Button
marginTop={8}
onClick={() => {
push("/configuration/datadog");
}}
>
Edit configuration
</Button>
</Box>
); );
} }
return null; return null;
}; };
const ConfigurationPage: NextPage = () => {
const selectedProvider = useActiveProvider();
return (
<AppColumnsLayout>
<IntegrationsList activeProvider={selectedProvider} />
<Content />
</AppColumnsLayout>
);
};
export default ConfigurationPage; export default ConfigurationPage;

View file

@ -1,16 +1,16 @@
import { NextPage } from "next"; import { NextPage } from "next";
import { useAppBridge } from "@saleor/app-sdk/app-bridge"; import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { MouseEventHandler, useEffect, useState } from "react"; import { useEffect } from "react";
import { LinearProgress, Link } from "@material-ui/core"; import { isInIframe } from "@saleor/apps-shared";
import { isInIframe } from "../lib/is-in-iframe";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Box, Text } from "@saleor/macaw-ui/next";
/** /**
* This is page publicly accessible from your app. * This is page publicly accessible from your app.
* You should probably remove it. * You should probably remove it.
*/ */
const IndexPage: NextPage = () => { const IndexPage: NextPage = () => {
const { appBridgeState, appBridge } = useAppBridge(); const { appBridgeState } = useAppBridge();
const { replace } = useRouter(); const { replace } = useRouter();
useEffect(() => { useEffect(() => {
@ -20,14 +20,16 @@ const IndexPage: NextPage = () => {
}, [appBridgeState?.ready, replace]); }, [appBridgeState?.ready, replace]);
if (isInIframe()) { if (isInIframe()) {
return <LinearProgress />; return <Text>Loading...</Text>;
} }
return ( return (
<div> <Box>
<h1>Saleor Monitoring</h1> <Text variant="heading" as="h1">
<p>Install App in Saleor to use it</p> Saleor Monitoring
</div> </Text>
<Text>Install App in Saleor Dashboard to use it</Text>
</Box>
); );
}; };

View file

@ -0,0 +1,3 @@
a {
text-decoration: none;
}

View file

@ -1,14 +0,0 @@
body {
font-family: Inter, -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
padding: 32px;
}
code {
border-radius: 5px;
display: inline-block;
margin-top: 10px;
padding: 0.75rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}

View file

@ -1,21 +0,0 @@
import { makeStyles } from "@saleor/macaw-ui";
import { PropsWithChildren } from "react";
const useStyles = makeStyles({
root: {
display: "grid",
gridTemplateColumns: "280px auto 280px",
alignItems: "start",
gap: 32,
maxWidth: 1180,
margin: "0 auto",
},
});
type Props = PropsWithChildren<{}>;
export function AppColumnsLayout({ children }: Props) {
const styles = useStyles();
return <div className={styles.root}>{children}</div>;
}

View file

@ -1,54 +1,24 @@
import { Controller, useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { import {
DataDogCredentialsInput, DataDogCredentialsInput,
DatadogSite, DatadogSite,
useConfigQuery, useConfigQuery,
Mutation,
useUpdateCredentialsMutation,
useDeleteDatadogCredentialsMutation, useDeleteDatadogCredentialsMutation,
useUpdateCredentialsMutation,
} from "../../../generated/graphql"; } from "../../../generated/graphql";
import { Section } from "../sections"; import { ArrowLeftIcon, Box, Button, Text } from "@saleor/macaw-ui/next";
import {
InputLabel, import React, { useEffect } from "react";
LinearProgress,
MenuItem,
Select,
TextField,
Typography,
Checkbox,
FormGroup,
FormControlLabel,
Link,
} from "@material-ui/core";
import React, { useEffect, useState } from "react";
import { Button, makeStyles, Backlink, IconButton } from "@saleor/macaw-ui";
import Image from "next/image"; import Image from "next/image";
import DatadogLogo from "../../assets/datadog/dd_logo_h_rgb.svg"; import DatadogLogo from "../../assets/datadog/dd_logo_h_rgb.svg";
import { gql, useMutation } from "urql"; import { gql } from "urql";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge"; import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
import { ArrowBack } from "@material-ui/icons";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { API_KEYS_LINKS } from "../../datadog-urls"; import { API_KEYS_LINKS } from "../../datadog-urls";
import { useDashboardNotification } from "@saleor/apps-shared"; import { useDashboardNotification } from "@saleor/apps-shared";
import { Input, Select, Toggle } from "@saleor/react-hook-form-macaw";
const useStyles = makeStyles({ import { Breadcrumbs } from "@saleor/apps-ui";
form: {
marginTop: 50,
display: "grid",
gridAutoFlow: "row",
gap: 30,
},
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
headline: {
marginRight: "auto",
marginLeft: 10,
},
});
gql` gql`
query Config { query Config {
@ -110,7 +80,7 @@ const ApiKeyHelperText = ({ site }: { site: DatadogSite }) => {
return ( return (
<span> <span>
Get one{" "} Get one{" "}
<Link <a
href={url} href={url}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
@ -123,13 +93,12 @@ const ApiKeyHelperText = ({ site }: { site: DatadogSite }) => {
}} }}
> >
here here
</Link> </a>
</span> </span>
); );
}; };
export const DatadogConfig = () => { export const DatadogConfig = () => {
const styles = useStyles();
const [queryData, fetchConfig] = useConfigQuery(); const [queryData, fetchConfig] = useConfigQuery();
const [, mutateCredentials] = useUpdateCredentialsMutation(); const [, mutateCredentials] = useUpdateCredentialsMutation();
const [, deleteCredentials] = useDeleteDatadogCredentialsMutation(); const [, deleteCredentials] = useDeleteDatadogCredentialsMutation();
@ -175,27 +144,36 @@ export const DatadogConfig = () => {
}, [queryData.data, setValue]); }, [queryData.data, setValue]);
if (queryData.fetching && !queryData.data) { if (queryData.fetching && !queryData.data) {
return <LinearProgress />; return <Text>Loading</Text>;
} }
return ( return (
<Section> <Box>
<div className={styles.header}> <Breadcrumbs>
<IconButton <Breadcrumbs.Item href={"/configuration"}>Configuration</Breadcrumbs.Item>
onClick={() => { <Breadcrumbs.Item>DataDog</Breadcrumbs.Item>
router.push("/configuration"); </Breadcrumbs>
}}
variant="secondary" <Box marginTop={8} display={"grid"} __gridTemplateColumns={"400px auto"} gap={8}>
> <Box display={"flex"} gap={4} flexDirection={"column"}>
<ArrowBack /> <Text variant={"heading"} as={"h1"}>
</IconButton>
<Typography className={styles.headline} variant="h3">
Configuration Configuration
</Typography> </Text>
<Image width={100} src={DatadogLogo} alt="DataDog" /> <Image width={100} src={DatadogLogo} alt="DataDog" />
</div> <Text as={"p"}>
<form Configure your Datadog integration to send your Saleor metrics to Datadog.
className={styles.form} </Text>
</Box>
<Box
display={"flex"}
gap={4}
flexDirection={"column"}
as={"form"}
borderColor={"neutralHighlight"}
borderWidth={1}
borderStyle={"solid"}
borderRadius={4}
padding={8}
onSubmit={handleSubmit((values) => { onSubmit={handleSubmit((values) => {
return mutateCredentials({ return mutateCredentials({
input: { input: {
@ -223,53 +201,34 @@ export const DatadogConfig = () => {
}); });
})} })}
> >
<div> <Box as={"label"} display={"flex"} gap={2}>
<Controller <Toggle control={control} name={"active"} />
render={({ field }) => { <Text variant={"bodyEmp"}>Active</Text>
return ( </Box>
<FormGroup row>
<FormControlLabel
control={<Checkbox checked={field.value} {...field} />}
label="Enabled"
/>
</FormGroup>
);
}}
name="active"
control={control}
/>
<InputLabel id="datadog-site-label">Datadog Site</InputLabel>
<Select <Select
defaultValue={DatadogSite.Us1} label={"Datadog Site"}
fullWidth options={Object.values(DatadogSite).map((v) => ({
labelId="datadog-site-label" label: v,
{...register("site")} value: v,
> }))}
{Object.values(DatadogSite).map((v) => ( control={control}
<MenuItem value={v} key={v}> name={"site"}
{v} />
</MenuItem> <Input
))} label="API Key"
</Select>
</div>
<TextField
fullWidth
variant="standard"
label="Api Key"
defaultValue="" defaultValue=""
helperText={<ApiKeyHelperText site={activeSite} />} helperText={<ApiKeyHelperText site={activeSite} />}
{...register("apiKey")} control={control}
name={"apiKey"}
/> />
{queryData.data?.integrations.datadog?.error && ( {queryData.data?.integrations.datadog?.error && (
<Typography color="error">{queryData.data?.integrations.datadog?.error}</Typography> <Text color={"textCriticalDefault"}>{queryData.data?.integrations.datadog?.error}</Text>
)} )}
<Button type="submit" variant="primary" fullWidth> <Box display={"flex"} gap={2} marginTop={8} justifyContent={"flex-end"}>
Save configuration
</Button>
<Button <Button
variant={"tertiary"}
type="reset" type="reset"
variant="secondary"
fullWidth
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
@ -277,12 +236,16 @@ export const DatadogConfig = () => {
fetchConfig(); fetchConfig();
reset(); reset();
notifySuccess("Configuration updated", "Successfully deleted Datadog settings"); notifySuccess("Configuration updated", "Successfully deleted Datadog settings");
router.push("/configuration");
}); });
}} }}
> >
Delete configuration <Text color={"textCriticalDefault"}>Delete configuration</Text>
</Button> </Button>
</form> <Button type="submit">Save configuration</Button>
</Section> </Box>
</Box>
</Box>
</Box>
); );
}; };

View file

@ -1,13 +1,14 @@
import { Section } from "./sections"; import { Box, Button, Text } from "@saleor/macaw-ui/next";
import { Typography } from "@material-ui/core"; import Link from "next/link";
export const NoProvidersConfigured = () => ( export const NoProvidersConfigured = () => (
<Section> <Box display={"flex"} gap={4} flexDirection={"column"}>
<Typography paragraph variant="h3"> <Text as={"h1"} variant="heading">
No providers configured No providers configured
</Typography> </Text>
<Typography paragraph> <Text as={"p"}>You need to configure Datadog to enable the app</Text>
Chose one of providers on the left and configure it to use the app <Link href={"/configuration/datadog"}>
</Typography> <Button>Configure Datadog</Button>
</Section> </Link>
</Box>
); );

View file

@ -1,99 +0,0 @@
import { makeStyles } from "@saleor/macaw-ui";
import Image from "next/image";
import DatadogLogo from "../assets/datadog/dd_logo_h_rgb.svg";
import NewRelicLogo from "../assets/new-relic/new_relic_logo_horizontal.svg";
import LogzLogo from "../assets/logzio/1584985593-blue-horizontal.svg";
import React from "react";
import { Section } from "./sections";
import { Typography } from "@material-ui/core";
import clsx from "clsx";
import { useRouter } from "next/router";
import { Done, Error } from "@material-ui/icons";
import { useConfigQuery } from "../../generated/graphql";
const useStyles = makeStyles((theme) => {
return {
item: {
cursor: "pointer",
display: "flex",
marginBottom: 20,
padding: "10px",
justifyContent: "space-between",
border: "1px solid transparent",
},
disabledItem: {
filter: "grayscale(1)",
opacity: 0.7,
pointerEvents: "none",
marginBottom: 20,
padding: "10px",
},
selected: {
border: `1px solid ${theme.palette.divider} !important`,
borderRadius: 4,
},
list: {
margin: 0,
padding: 0,
listStyle: "none",
},
};
});
type Props = {
activeProvider: "datadog" | string | null;
};
export const IntegrationsList = ({ activeProvider }: Props) => {
const styles = useStyles();
const router = useRouter();
const [queryData] = useConfigQuery();
const isDatadogConfigured = queryData.data?.integrations.datadog?.credentials;
const isDatadogError = queryData.data?.integrations.datadog?.error;
return (
<Section>
<ul className={styles.list}>
<li
onClick={() => {
router.push("/configuration/datadog");
}}
className={clsx(styles.item, {
[styles.selected]: activeProvider === "datadog",
})}
>
<div>
<Image alt="Datadog" width={100} src={DatadogLogo} />
</div>
{isDatadogConfigured && !isDatadogError && (
<div>
<Done color="secondary" />
</div>
)}
{isDatadogError && (
<div>
<Error color="error" />
</div>
)}
</li>
<li className={styles.disabledItem}>
<div>
<Typography variant="caption">Coming Soon</Typography>
</div>
</li>
<li className={styles.disabledItem}>
<div>
<Image alt="New Relic" width={100} src={NewRelicLogo} />
</div>
</li>
<li className={styles.disabledItem}>
<div>
<Image alt="Logz.io" width={100} src={LogzLogo} />
</div>
</li>
</ul>
</Section>
);
};

View file

@ -1,6 +0,0 @@
import { Paper, PaperProps } from "@material-ui/core";
import React from "react";
export const Section = (props: PaperProps) => (
<Paper {...props} elevation={0} style={{ padding: 20 }} />
);

View file

@ -17,8 +17,8 @@
"@saleor/app-sdk": "0.40.1", "@saleor/app-sdk": "0.40.1",
"@saleor/apps-shared": "workspace:*", "@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*", "@saleor/apps-ui": "workspace:*",
"@saleor/react-hook-form-macaw": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.95", "@saleor/macaw-ui": "0.8.0-pre.95",
"@saleor/react-hook-form-macaw": "workspace:*",
"@sentry/nextjs": "7.55.2", "@sentry/nextjs": "7.55.2",
"@urql/exchange-auth": "^2.1.4", "@urql/exchange-auth": "^2.1.4",
"clsx": "^1.2.1", "clsx": "^1.2.1",

View file

@ -886,9 +886,15 @@ importers:
'@saleor/apps-shared': '@saleor/apps-shared':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/shared version: link:../../packages/shared
'@saleor/apps-ui':
specifier: workspace:*
version: link:../../packages/ui
'@saleor/macaw-ui': '@saleor/macaw-ui':
specifier: ^0.7.2 specifier: 0.8.0-pre.95
version: 0.7.3(@material-ui/core@4.12.4)(@material-ui/icons@4.11.3)(@material-ui/lab@4.0.0-alpha.61)(@types/react@18.2.5)(react-dom@18.2.0)(react-helmet@6.1.0)(react@18.2.0) version: 0.8.0-pre.95(@types/react-dom@18.2.5)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
'@saleor/react-hook-form-macaw':
specifier: workspace:*
version: link:../../packages/react-hook-form-macaw
'@urql/exchange-auth': '@urql/exchange-auth':
specifier: ^2.1.4 specifier: ^2.1.4
version: 2.1.4(graphql@16.6.0) version: 2.1.4(graphql@16.6.0)
@ -923,7 +929,7 @@ importers:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
react-hook-form: react-hook-form:
specifier: ^7.42.1 specifier: ^7.43.9
version: 7.44.3(react@18.2.0) version: 7.44.3(react@18.2.0)
urql: urql:
specifier: ^4.0.4 specifier: ^4.0.4