feat: add dashboard UI for Core Extensions app
- Created dashboard page with webhook status - Added email configuration accordion - Shows connection status to Saleor - Displays app information and quick actions - Auto-redirects to dashboard when in Saleor iframe
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
Button,
|
||||
Chip,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
} from "@saleor/macaw-ui";
|
||||
import { NextPage } from "next";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface WebhookStatus {
|
||||
name: string;
|
||||
event: string;
|
||||
active: boolean;
|
||||
targetUrl: string;
|
||||
}
|
||||
|
||||
interface AppInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const DashboardPage: NextPage = () => {
|
||||
const { appBridgeState } = useAppBridge();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
// Simulate loading app data
|
||||
setTimeout(() => setLoading(false), 1000);
|
||||
}, []);
|
||||
|
||||
const webhooks: WebhookStatus[] = [
|
||||
{
|
||||
name: "Order Created",
|
||||
event: "ORDER_CREATED",
|
||||
active: true,
|
||||
targetUrl: "N8N Workflow",
|
||||
},
|
||||
{
|
||||
name: "Order Fulfilled",
|
||||
event: "ORDER_FULFILLED",
|
||||
active: true,
|
||||
targetUrl: "N8N Workflow",
|
||||
},
|
||||
{
|
||||
name: "Order Cancelled",
|
||||
event: "ORDER_CANCELLED",
|
||||
active: true,
|
||||
targetUrl: "N8N Workflow",
|
||||
},
|
||||
];
|
||||
|
||||
const appInfo: AppInfo = {
|
||||
name: "Core Extensions",
|
||||
version: "1.0.0",
|
||||
id: "saleor-core-extensions",
|
||||
};
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" height="100vh">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box padding={8}>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<CircularProgress size={24} />
|
||||
<Text>Loading dashboard...</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box padding={8}>
|
||||
{/* Header */}
|
||||
<Box marginBottom={8}>
|
||||
<Text size={11} as="h1">
|
||||
Core Extensions Dashboard
|
||||
</Text>
|
||||
<Text color="textSecondary" marginTop={2}>
|
||||
Email automation for ManoonOils
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Connection Status */}
|
||||
<Box marginBottom={8}>
|
||||
<Text size={8} as="h2" marginBottom={4}>
|
||||
Connection Status
|
||||
</Text>
|
||||
{appBridgeState?.ready ? (
|
||||
<Alert severity="success">
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Chip size="small" color="success">
|
||||
Connected
|
||||
</Chip>
|
||||
<Text>Connected to Saleor Dashboard</Text>
|
||||
</Box>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert severity="warning">
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Chip size="small" color="warning">
|
||||
Standalone
|
||||
</Chip>
|
||||
<Text>Running outside Saleor Dashboard</Text>
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Webhooks Section */}
|
||||
<Box marginY={8}>
|
||||
<Text size={8} as="h2" marginBottom={4}>
|
||||
Webhooks Configuration
|
||||
</Text>
|
||||
<Text marginBottom={4} color="textSecondary">
|
||||
Active webhooks forwarding to N8N
|
||||
</Text>
|
||||
|
||||
<List>
|
||||
{webhooks.map((webhook) => (
|
||||
<ListItem key={webhook.event}>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
>
|
||||
<ListItemText
|
||||
primary={webhook.name}
|
||||
secondary={webhook.event}
|
||||
/>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<Text color="textSecondary" size={2}>
|
||||
{webhook.targetUrl}
|
||||
</Text>
|
||||
<Chip size="small" color={webhook.active ? "success" : "error"}>
|
||||
{webhook.active ? "Active" : "Inactive"}
|
||||
</Chip>
|
||||
</Box>
|
||||
</Box>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Email Configuration */}
|
||||
<Box marginY={8}>
|
||||
<Text size={8} as="h2" marginBottom={4}>
|
||||
Email Configuration
|
||||
</Text>
|
||||
|
||||
<Accordion>
|
||||
<AccordionSummary>
|
||||
<Text>Customer Emails</Text>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>From:</Text>
|
||||
<Text>support@mail.manoonoils.com</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>Provider:</Text>
|
||||
<Text>Resend</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>Templates:</Text>
|
||||
<Text>Order Confirmation, Order Shipped, Order Cancelled</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
<Accordion>
|
||||
<AccordionSummary>
|
||||
<Text>Admin Notifications</Text>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>Recipients:</Text>
|
||||
<Text>me@hytham.me, tamara@hytham.me</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>Subject:</Text>
|
||||
<Text>New Order! 🎉 #{orderNumber}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>Trigger:</Text>
|
||||
<Text>All order events</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* App Info */}
|
||||
<Box marginY={8}>
|
||||
<Text size={8} as="h2" marginBottom={4}>
|
||||
App Information
|
||||
</Text>
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>Name:</Text>
|
||||
<Text>{appInfo.name}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>Version:</Text>
|
||||
<Text>{appInfo.version}</Text>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>ID:</Text>
|
||||
<Text>{appInfo.id}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Actions */}
|
||||
<Box marginTop={8} display="flex" gap={4}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
window.open("https://n8n.nodecrew.me", "_blank");
|
||||
}}
|
||||
>
|
||||
Open N8N
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
window.open("https://resend.com/emails", "_blank");
|
||||
}}
|
||||
>
|
||||
View Resend Emails
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardPage;
|
||||
+73
-183
@@ -1,206 +1,96 @@
|
||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { Box, Button, Input, Text } from "@saleor/macaw-ui";
|
||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { Box, Text, Button, CircularProgress } from "@saleor/macaw-ui";
|
||||
import { NextPage } from "next";
|
||||
import Link from "next/link";
|
||||
import { MouseEventHandler, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const AddToSaleorForm = () => (
|
||||
<Box
|
||||
as={"form"}
|
||||
display={"flex"}
|
||||
alignItems={"center"}
|
||||
gap={4}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const saleorUrl = new FormData(event.currentTarget as HTMLFormElement).get("saleor-url");
|
||||
const manifestUrl = new URL("/api/manifest", window.location.origin);
|
||||
const redirectUrl = new URL(
|
||||
`/dashboard/apps/install?manifestUrl=${manifestUrl}`,
|
||||
saleorUrl as string
|
||||
).href;
|
||||
|
||||
window.open(redirectUrl, "_blank");
|
||||
}}
|
||||
>
|
||||
<Input type="url" required label="Saleor URL" name="saleor-url" />
|
||||
<Button type="submit">Add to Saleor</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
/**
|
||||
* This is page publicly accessible from your app.
|
||||
* You should probably remove it.
|
||||
*/
|
||||
const IndexPage: NextPage = () => {
|
||||
const { appBridgeState, appBridge } = useAppBridge();
|
||||
const { appBridgeState } = useAppBridge();
|
||||
const router = useRouter();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const handleLinkClick: MouseEventHandler<HTMLAnchorElement> = (e) => {
|
||||
/**
|
||||
* In iframe, link can't be opened in new tab, so Dashboard must be a proxy
|
||||
*/
|
||||
useEffect(() => {
|
||||
// If inside Saleor Dashboard, redirect to dashboard
|
||||
if (appBridgeState?.ready) {
|
||||
e.preventDefault();
|
||||
|
||||
appBridge?.dispatch(
|
||||
actions.Redirect({
|
||||
newContext: true,
|
||||
to: e.currentTarget.href,
|
||||
})
|
||||
);
|
||||
router.push("/dashboard");
|
||||
}
|
||||
}, [appBridgeState?.ready, router]);
|
||||
|
||||
/**
|
||||
* Otherwise, assume app is accessed outside of Dashboard, so href attribute on <a> will work
|
||||
*/
|
||||
};
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" height="100vh">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const isLocalHost = global.location.href.includes("localhost");
|
||||
// If inside Saleor Dashboard, show loading while redirecting
|
||||
if (appBridgeState?.ready) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" height="100vh">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Standalone landing page
|
||||
return (
|
||||
<Box padding={8}>
|
||||
<Text size={11}>Welcome to Saleor App Template (Next.js) 🚀</Text>
|
||||
<Text as={"p"} marginY={4}>
|
||||
Saleor App Template is a minimalistic boilerplate that provides a working example of a
|
||||
Saleor app.
|
||||
<Box padding={8} maxWidth={800} margin="0 auto">
|
||||
<Text size={11} as="h1" marginBottom={4}>
|
||||
Core Extensions
|
||||
</Text>
|
||||
{appBridgeState?.ready && mounted && (
|
||||
<Link href="/actions">
|
||||
<Button variant="secondary">See what your app can do →</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Text as={"p"} marginTop={8}>
|
||||
Explore the App Template by visiting:
|
||||
<Text size={6} color="textSecondary" marginBottom={8}>
|
||||
Email automation for ManoonOils
|
||||
</Text>
|
||||
<ul>
|
||||
<li>
|
||||
<code>/src/pages/api/manifest</code> - the{" "}
|
||||
<a
|
||||
href="https://docs.saleor.io/docs/3.x/developer/extending/apps/manifest"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
App Manifest
|
||||
</a>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
<code>/src/pages/api/webhooks/order-created</code> - an example <code>ORDER_CREATED</code>{" "}
|
||||
webhook handler.
|
||||
</li>
|
||||
<li>
|
||||
<code>/graphql</code> - the pre-defined GraphQL queries.
|
||||
</li>
|
||||
<li>
|
||||
<code>/generated/graphql.ts</code> - the code generated for those queries by{" "}
|
||||
<a target="_blank" rel="noreferrer" href="https://the-guild.dev/graphql/codegen">
|
||||
GraphQL Code Generator
|
||||
</a>
|
||||
.
|
||||
</li>
|
||||
</ul>
|
||||
<Text size={8} marginTop={8} as={"h2"}>
|
||||
Resources
|
||||
</Text>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
onClick={handleLinkClick}
|
||||
target="_blank"
|
||||
href="https://docs.saleor.io/docs/3.x/developer/extending/apps/key-concepts"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Text color={"info1"}>Apps documentation </Text>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={handleLinkClick}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://docs.saleor.io/docs/3.x/developer/extending/apps/developing-with-tunnels"
|
||||
>
|
||||
<Text color={"info1"}>Tunneling the app</Text>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={handleLinkClick}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://github.com/saleor/app-examples"
|
||||
>
|
||||
<Text color={"info1"}>App Examples repository</Text>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
onClick={handleLinkClick}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://github.com/saleor/saleor-app-sdk"
|
||||
>
|
||||
<Text color={"info1"}>Saleor App SDK</Text>
|
||||
</a>
|
||||
</li>
|
||||
<Box
|
||||
as="div"
|
||||
backgroundColor="surfaceNeutralPlain"
|
||||
padding={8}
|
||||
borderRadius={4}
|
||||
marginBottom={8}
|
||||
>
|
||||
<Text size={7} marginBottom={4}>
|
||||
Features
|
||||
</Text>
|
||||
<Box as="ul" display="flex" flexDirection="column" gap={2}>
|
||||
<li>✉️ Automated order confirmation emails</li>
|
||||
<li>📦 Shipping notification emails</li>
|
||||
<li>❌ Order cancellation emails</li>
|
||||
<li>🔔 Admin notifications for all orders</li>
|
||||
<li>🎨 Styled HTML email templates</li>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<li>
|
||||
<a
|
||||
onClick={handleLinkClick}
|
||||
target="_blank"
|
||||
href="https://github.com/saleor/saleor-cli"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Text color={"info1"}>Saleor CLI</Text>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={handleLinkClick}
|
||||
target="_blank"
|
||||
href="https://github.com/saleor/apps"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Text color={"info1"}>Saleor App Store - official apps by Saleor Team</Text>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={handleLinkClick}
|
||||
target="_blank"
|
||||
href="https://macaw-ui-next.vercel.app/?path=/docs/getting-started-installation--docs"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Text color={"info1"}>Macaw UI - official Saleor UI library</Text>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
onClick={handleLinkClick}
|
||||
target="_blank"
|
||||
href="https://nextjs.org/docs"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Text color={"info1"}>Next.js documentation</Text>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<Box display="flex" flexDirection="column" gap={4}>
|
||||
<Text>
|
||||
This app connects Saleor to N8N for automated email workflows using Resend.
|
||||
</Text>
|
||||
|
||||
{mounted && !isLocalHost && !appBridgeState?.ready && (
|
||||
<>
|
||||
<Text marginBottom={4} as={"p"}>
|
||||
Install this app in your Dashboard and get extra powers!
|
||||
</Text>
|
||||
<AddToSaleorForm />
|
||||
</>
|
||||
)}
|
||||
<Box display="flex" gap={4} marginTop={4}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
window.open("https://dashboard.manoonoils.com/apps", "_blank");
|
||||
}}
|
||||
>
|
||||
Open in Saleor Dashboard
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
router.push("/dashboard");
|
||||
}}
|
||||
>
|
||||
View Dashboard
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user