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:
parent
f6ff907cfd
commit
3bd7e3f05d
24 changed files with 267 additions and 526 deletions
5
.changeset/twenty-apricots-own.md
Normal file
5
.changeset/twenty-apricots-own.md
Normal 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.
|
1
apps/monitoring/.env.example
Normal file
1
apps/monitoring/.env.example
Normal file
|
@ -0,0 +1 @@
|
|||
MONITORING_APP_API_URL=
|
|
@ -55,6 +55,8 @@ To use Graphql Playground, `Monitoring` app needs to be installed in Saleor, and
|
|||
|
||||
### Testing DataDog integration
|
||||
|
||||
Set `MOCK_DATADOG_CLIENT` env to `True`
|
||||
|
||||
Use these credentials sets to test DataDog integration:
|
||||
|
||||
Working credentials:
|
||||
|
|
|
@ -12,5 +12,7 @@ services:
|
|||
- "5001:80"
|
||||
environment:
|
||||
- DEBUG=True
|
||||
# Uncomment to enable test credentials mode
|
||||
# - MOCK_DATADOG_CLIENT=True
|
||||
volumes:
|
||||
- ./backend/monitoring/:/app/monitoring
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
module.exports = {
|
||||
transpilePackages: ["@saleor/apps-shared"],
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
|
||||
rewrites() {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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 {
|
||||
fallback: [
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"@saleor/app-sdk": "0.40.1",
|
||||
"@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",
|
||||
"@vitejs/plugin-react": "4.0.0",
|
||||
"clsx": "^1.2.1",
|
||||
|
@ -28,7 +30,7 @@
|
|||
"pino-pretty": "^10.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.42.1",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"urql": "^4.0.4",
|
||||
"vite": "4.3.9",
|
||||
"vitest": "0.31.3"
|
||||
|
@ -50,8 +52,5 @@
|
|||
"eslint-config-saleor": "workspace:*",
|
||||
"typescript": "5.1.3"
|
||||
},
|
||||
"private": true,
|
||||
"saleor": {
|
||||
"schemaVersion": "3.10"
|
||||
}
|
||||
"private": true
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ export const API_KEYS_LINKS: { [key in DatadogSite]: string } = {
|
|||
};
|
||||
|
||||
export const DATADOG_SITES_LINKS: { [key in DatadogSite]: string } = {
|
||||
[DatadogSite.Us1]: "datadoghq.com",
|
||||
[DatadogSite.Us3]: "us3.datadoghq.com",
|
||||
[DatadogSite.Us5]: "us5.datadoghq.com",
|
||||
[DatadogSite.Eu1]: "datadoghq.eu",
|
||||
[DatadogSite.Us1Fed]: "ddog-gov.com",
|
||||
[DatadogSite.Us1]: "https://datadoghq.com",
|
||||
[DatadogSite.Us3]: "https://us3.datadoghq.com",
|
||||
[DatadogSite.Us5]: "https://us5.datadoghq.com",
|
||||
[DatadogSite.Eu1]: "https://datadoghq.eu",
|
||||
[DatadogSite.Us1Fed]: "https://ddog-gov.com",
|
||||
};
|
||||
|
|
|
@ -1,7 +1,49 @@
|
|||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||
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<{}>) {
|
||||
const { appBridgeState } = useAppBridge();
|
||||
|
@ -11,6 +53,7 @@ export function GraphQLProvider(props: PropsWithChildren<{}>) {
|
|||
const client = createGraphQLClient({
|
||||
saleorApiUrl,
|
||||
token,
|
||||
graphql: "/graphql/",
|
||||
});
|
||||
|
||||
return <Provider value={client} {...props} />;
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
export function isInIframe() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,33 +1,25 @@
|
|||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { useTheme } from "@saleor/macaw-ui";
|
||||
import { memo, useEffect } from "react";
|
||||
import { useTheme } from "@saleor/macaw-ui/next";
|
||||
import { useEffect } from "react";
|
||||
|
||||
/**
|
||||
* Macaw-ui stores its theme mode in memory and local storage. To synchronize App with Dashboard,
|
||||
* Macaw must be informed about this change from AppBridge.
|
||||
*
|
||||
* If you are not using Macaw, you can remove this.
|
||||
*/
|
||||
function _ThemeSynchronizer() {
|
||||
// todo move to shared
|
||||
export function ThemeSynchronizer() {
|
||||
const { appBridgeState } = useAppBridge();
|
||||
const { setTheme, themeType } = useTheme();
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (!setTheme || !appBridgeState?.theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (themeType !== appBridgeState?.theme) {
|
||||
setTheme(appBridgeState.theme);
|
||||
/**
|
||||
* 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);
|
||||
if (appBridgeState.theme === "light") {
|
||||
setTheme("defaultLight");
|
||||
}
|
||||
}, [appBridgeState?.theme, setTheme, themeType]);
|
||||
|
||||
if (appBridgeState.theme === "dark") {
|
||||
setTheme("defaultDark");
|
||||
}
|
||||
}, [appBridgeState?.theme, setTheme]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const ThemeSynchronizer = memo(_ThemeSynchronizer);
|
||||
|
|
|
@ -1,88 +1,30 @@
|
|||
import "../styles/globals.css";
|
||||
|
||||
import { Theme } from "@material-ui/core/styles";
|
||||
import "@saleor/macaw-ui/next/style";
|
||||
import "../style.css";
|
||||
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
|
||||
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
|
||||
import {
|
||||
dark,
|
||||
light,
|
||||
SaleorThemeColors,
|
||||
ThemeProvider as MacawUIThemeProvider,
|
||||
} from "@saleor/macaw-ui";
|
||||
import React, { PropsWithChildren, useEffect } from "react";
|
||||
import React from "react";
|
||||
import { AppProps } from "next/app";
|
||||
|
||||
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
|
||||
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";
|
||||
|
||||
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.
|
||||
* TODO: This is React 18 issue, consider hiding this workaround inside app-sdk
|
||||
*/
|
||||
const appBridgeInstance =
|
||||
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 }>
|
||||
>;
|
||||
export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
|
||||
|
||||
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 (
|
||||
<NoSSRWrapper>
|
||||
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
|
||||
<GraphQLProvider>
|
||||
<ThemeProvider overrides={themeOverrides} ssr palettes={palettes}>
|
||||
<ThemeProvider>
|
||||
<ThemeSynchronizer />
|
||||
<RoutePropagator />
|
||||
<Box padding={4}>
|
||||
<Component {...pageProps} />
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
</GraphQLProvider>
|
||||
</AppBridgeProvider>
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import { NextPage } from "next";
|
||||
import { AppColumnsLayout } from "../../ui/app-columns-layout";
|
||||
import React, { useEffect } from "react";
|
||||
import { IntegrationsList } from "../../ui/providers-list";
|
||||
import { NoProvidersConfigured } from "../../ui/no-providers-configured";
|
||||
import { useRouter } from "next/router";
|
||||
import { DatadogConfig } from "../../ui/datadog/datadog-config";
|
||||
import { DatadogSite, useConfigQuery } from "../../../generated/graphql";
|
||||
import { LinearProgress, Link, Typography } from "@material-ui/core";
|
||||
import { Section } from "../../ui/sections";
|
||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { Done, Error } from "@material-ui/icons";
|
||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
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 router = useRouter();
|
||||
|
@ -20,13 +17,15 @@ const useActiveProvider = () => {
|
|||
return selectedProvider ?? null;
|
||||
};
|
||||
|
||||
const Content = () => {
|
||||
const ConfigurationPage = () => {
|
||||
const [configuration, fetchConfiguration] = useConfigQuery();
|
||||
const { appBridge } = useAppBridge();
|
||||
|
||||
const datadogCredentials = configuration.data?.integrations.datadog?.credentials;
|
||||
const datadogError = configuration.data?.integrations.datadog?.error;
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfiguration();
|
||||
}, [fetchConfiguration]);
|
||||
|
@ -34,7 +33,7 @@ const Content = () => {
|
|||
const selectedProvider = useActiveProvider();
|
||||
|
||||
if (configuration.fetching && !configuration.data) {
|
||||
return <LinearProgress />;
|
||||
return <Text>Loading...</Text>;
|
||||
}
|
||||
|
||||
if (selectedProvider === "datadog") {
|
||||
|
@ -50,57 +49,48 @@ const Content = () => {
|
|||
const site = configuration.data?.integrations.datadog?.credentials.site ?? DatadogSite.Us1;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<Typography paragraph variant="h3">
|
||||
<Done style={{ verticalAlign: "middle", marginRight: 10 }} />
|
||||
<Box display={"flex"} gap={4} flexDirection={"column"}>
|
||||
<Text as={"h1"} variant="heading">
|
||||
App configured
|
||||
</Typography>
|
||||
<Typography paragraph>
|
||||
</Text>
|
||||
<Text as={"p"}>
|
||||
Visit{" "}
|
||||
<Link
|
||||
href="https://app.datadoghq.com/"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
appBridge?.dispatch(
|
||||
actions.Redirect({
|
||||
to: DATADOG_SITES_LINKS[site],
|
||||
newContext: true,
|
||||
})
|
||||
);
|
||||
<TextLink newTab href={DATADOG_SITES_LINKS[site] ?? "https://app.datadoghq.com/"}>
|
||||
Datadog
|
||||
</TextLink>{" "}
|
||||
to access your logs
|
||||
</Text>
|
||||
<Button
|
||||
onClick={() => {
|
||||
push("/configuration/datadog");
|
||||
}}
|
||||
>
|
||||
Datadog
|
||||
</Link>{" "}
|
||||
to access your logs
|
||||
</Typography>
|
||||
</Section>
|
||||
Edit configuration
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (datadogError) {
|
||||
return (
|
||||
<Section>
|
||||
<Typography paragraph variant="h3">
|
||||
<Error style={{ verticalAlign: "middle", marginRight: 10 }} />
|
||||
<Box>
|
||||
<Text variant="heading" as={"h1"}>
|
||||
Configuration Error
|
||||
</Typography>
|
||||
<Typography>{datadogError}</Typography>
|
||||
</Section>
|
||||
</Text>
|
||||
<Text color={"textCriticalDefault"}>{datadogError}</Text>
|
||||
<Button
|
||||
marginTop={8}
|
||||
onClick={() => {
|
||||
push("/configuration/datadog");
|
||||
}}
|
||||
>
|
||||
Edit configuration
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ConfigurationPage: NextPage = () => {
|
||||
const selectedProvider = useActiveProvider();
|
||||
|
||||
return (
|
||||
<AppColumnsLayout>
|
||||
<IntegrationsList activeProvider={selectedProvider} />
|
||||
<Content />
|
||||
</AppColumnsLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigurationPage;
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { NextPage } from "next";
|
||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { MouseEventHandler, useEffect, useState } from "react";
|
||||
import { LinearProgress, Link } from "@material-ui/core";
|
||||
import { isInIframe } from "../lib/is-in-iframe";
|
||||
import { useEffect } from "react";
|
||||
import { isInIframe } from "@saleor/apps-shared";
|
||||
import { useRouter } from "next/router";
|
||||
import { Box, Text } from "@saleor/macaw-ui/next";
|
||||
|
||||
/**
|
||||
* This is page publicly accessible from your app.
|
||||
* You should probably remove it.
|
||||
*/
|
||||
const IndexPage: NextPage = () => {
|
||||
const { appBridgeState, appBridge } = useAppBridge();
|
||||
const { appBridgeState } = useAppBridge();
|
||||
const { replace } = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -20,14 +20,16 @@ const IndexPage: NextPage = () => {
|
|||
}, [appBridgeState?.ready, replace]);
|
||||
|
||||
if (isInIframe()) {
|
||||
return <LinearProgress />;
|
||||
return <Text>Loading...</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Saleor Monitoring</h1>
|
||||
<p>Install App in Saleor to use it</p>
|
||||
</div>
|
||||
<Box>
|
||||
<Text variant="heading" as="h1">
|
||||
Saleor Monitoring
|
||||
</Text>
|
||||
<Text>Install App in Saleor Dashboard to use it</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
3
apps/monitoring/src/style.css
Normal file
3
apps/monitoring/src/style.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
a {
|
||||
text-decoration: none;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -1,54 +1,24 @@
|
|||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
DataDogCredentialsInput,
|
||||
DatadogSite,
|
||||
useConfigQuery,
|
||||
Mutation,
|
||||
useUpdateCredentialsMutation,
|
||||
useDeleteDatadogCredentialsMutation,
|
||||
useUpdateCredentialsMutation,
|
||||
} from "../../../generated/graphql";
|
||||
import { Section } from "../sections";
|
||||
import {
|
||||
InputLabel,
|
||||
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 { ArrowLeftIcon, Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
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 { ArrowBack } from "@material-ui/icons";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { API_KEYS_LINKS } from "../../datadog-urls";
|
||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
form: {
|
||||
marginTop: 50,
|
||||
display: "grid",
|
||||
gridAutoFlow: "row",
|
||||
gap: 30,
|
||||
},
|
||||
header: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
headline: {
|
||||
marginRight: "auto",
|
||||
marginLeft: 10,
|
||||
},
|
||||
});
|
||||
import { Input, Select, Toggle } from "@saleor/react-hook-form-macaw";
|
||||
import { Breadcrumbs } from "@saleor/apps-ui";
|
||||
|
||||
gql`
|
||||
query Config {
|
||||
|
@ -110,7 +80,7 @@ const ApiKeyHelperText = ({ site }: { site: DatadogSite }) => {
|
|||
return (
|
||||
<span>
|
||||
Get one{" "}
|
||||
<Link
|
||||
<a
|
||||
href={url}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -123,13 +93,12 @@ const ApiKeyHelperText = ({ site }: { site: DatadogSite }) => {
|
|||
}}
|
||||
>
|
||||
here
|
||||
</Link>
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatadogConfig = () => {
|
||||
const styles = useStyles();
|
||||
const [queryData, fetchConfig] = useConfigQuery();
|
||||
const [, mutateCredentials] = useUpdateCredentialsMutation();
|
||||
const [, deleteCredentials] = useDeleteDatadogCredentialsMutation();
|
||||
|
@ -175,27 +144,36 @@ export const DatadogConfig = () => {
|
|||
}, [queryData.data, setValue]);
|
||||
|
||||
if (queryData.fetching && !queryData.data) {
|
||||
return <LinearProgress />;
|
||||
return <Text>Loading</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<div className={styles.header}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
router.push("/configuration");
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
<Typography className={styles.headline} variant="h3">
|
||||
<Box>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item href={"/configuration"}>Configuration</Breadcrumbs.Item>
|
||||
<Breadcrumbs.Item>DataDog</Breadcrumbs.Item>
|
||||
</Breadcrumbs>
|
||||
|
||||
<Box marginTop={8} display={"grid"} __gridTemplateColumns={"400px auto"} gap={8}>
|
||||
<Box display={"flex"} gap={4} flexDirection={"column"}>
|
||||
<Text variant={"heading"} as={"h1"}>
|
||||
Configuration
|
||||
</Typography>
|
||||
</Text>
|
||||
<Image width={100} src={DatadogLogo} alt="DataDog" />
|
||||
</div>
|
||||
<form
|
||||
className={styles.form}
|
||||
<Text as={"p"}>
|
||||
Configure your Datadog integration to send your Saleor metrics to Datadog.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
display={"flex"}
|
||||
gap={4}
|
||||
flexDirection={"column"}
|
||||
as={"form"}
|
||||
borderColor={"neutralHighlight"}
|
||||
borderWidth={1}
|
||||
borderStyle={"solid"}
|
||||
borderRadius={4}
|
||||
padding={8}
|
||||
onSubmit={handleSubmit((values) => {
|
||||
return mutateCredentials({
|
||||
input: {
|
||||
|
@ -223,53 +201,34 @@ export const DatadogConfig = () => {
|
|||
});
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<Controller
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormGroup row>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={field.value} {...field} />}
|
||||
label="Enabled"
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}}
|
||||
name="active"
|
||||
control={control}
|
||||
/>
|
||||
<InputLabel id="datadog-site-label">Datadog Site</InputLabel>
|
||||
<Box as={"label"} display={"flex"} gap={2}>
|
||||
<Toggle control={control} name={"active"} />
|
||||
<Text variant={"bodyEmp"}>Active</Text>
|
||||
</Box>
|
||||
|
||||
<Select
|
||||
defaultValue={DatadogSite.Us1}
|
||||
fullWidth
|
||||
labelId="datadog-site-label"
|
||||
{...register("site")}
|
||||
>
|
||||
{Object.values(DatadogSite).map((v) => (
|
||||
<MenuItem value={v} key={v}>
|
||||
{v}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="standard"
|
||||
label="Api Key"
|
||||
label={"Datadog Site"}
|
||||
options={Object.values(DatadogSite).map((v) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
}))}
|
||||
control={control}
|
||||
name={"site"}
|
||||
/>
|
||||
<Input
|
||||
label="API Key"
|
||||
defaultValue=""
|
||||
helperText={<ApiKeyHelperText site={activeSite} />}
|
||||
{...register("apiKey")}
|
||||
control={control}
|
||||
name={"apiKey"}
|
||||
/>
|
||||
{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>
|
||||
Save configuration
|
||||
</Button>
|
||||
<Box display={"flex"} gap={2} marginTop={8} justifyContent={"flex-end"}>
|
||||
<Button
|
||||
variant={"tertiary"}
|
||||
type="reset"
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -277,12 +236,16 @@ export const DatadogConfig = () => {
|
|||
fetchConfig();
|
||||
reset();
|
||||
notifySuccess("Configuration updated", "Successfully deleted Datadog settings");
|
||||
router.push("/configuration");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete configuration
|
||||
<Text color={"textCriticalDefault"}>Delete configuration</Text>
|
||||
</Button>
|
||||
</form>
|
||||
</Section>
|
||||
<Button type="submit">Save configuration</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { Section } from "./sections";
|
||||
import { Typography } from "@material-ui/core";
|
||||
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||
import Link from "next/link";
|
||||
|
||||
export const NoProvidersConfigured = () => (
|
||||
<Section>
|
||||
<Typography paragraph variant="h3">
|
||||
<Box display={"flex"} gap={4} flexDirection={"column"}>
|
||||
<Text as={"h1"} variant="heading">
|
||||
No providers configured
|
||||
</Typography>
|
||||
<Typography paragraph>
|
||||
Chose one of providers on the left and configure it to use the app
|
||||
</Typography>
|
||||
</Section>
|
||||
</Text>
|
||||
<Text as={"p"}>You need to configure Datadog to enable the app</Text>
|
||||
<Link href={"/configuration/datadog"}>
|
||||
<Button>Configure Datadog</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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 }} />
|
||||
);
|
|
@ -17,8 +17,8 @@
|
|||
"@saleor/app-sdk": "0.40.1",
|
||||
"@saleor/apps-shared": "workspace:*",
|
||||
"@saleor/apps-ui": "workspace:*",
|
||||
"@saleor/react-hook-form-macaw": "workspace:*",
|
||||
"@saleor/macaw-ui": "0.8.0-pre.95",
|
||||
"@saleor/react-hook-form-macaw": "workspace:*",
|
||||
"@sentry/nextjs": "7.55.2",
|
||||
"@urql/exchange-auth": "^2.1.4",
|
||||
"clsx": "^1.2.1",
|
||||
|
|
|
@ -886,9 +886,15 @@ importers:
|
|||
'@saleor/apps-shared':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shared
|
||||
'@saleor/apps-ui':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ui
|
||||
'@saleor/macaw-ui':
|
||||
specifier: ^0.7.2
|
||||
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)
|
||||
specifier: 0.8.0-pre.95
|
||||
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':
|
||||
specifier: ^2.1.4
|
||||
version: 2.1.4(graphql@16.6.0)
|
||||
|
@ -923,7 +929,7 @@ importers:
|
|||
specifier: 18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
react-hook-form:
|
||||
specifier: ^7.42.1
|
||||
specifier: ^7.43.9
|
||||
version: 7.44.3(react@18.2.0)
|
||||
urql:
|
||||
specifier: ^4.0.4
|
||||
|
|
Loading…
Reference in a new issue