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:
Unchained
2026-03-27 18:34:53 +02:00
parent a2601a3d3c
commit b54299d2c3
2 changed files with 340 additions and 183 deletions
+267
View File
@@ -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
View File
@@ -1,206 +1,96 @@
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge"; import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { Box, Button, Input, Text } from "@saleor/macaw-ui"; import { Box, Text, Button, CircularProgress } from "@saleor/macaw-ui";
import { NextPage } from "next"; import { NextPage } from "next";
import Link from "next/link"; import { useEffect, useState } from "react";
import { MouseEventHandler, 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 IndexPage: NextPage = () => {
const { appBridgeState, appBridge } = useAppBridge(); const { appBridgeState } = useAppBridge();
const router = useRouter();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
}, []); }, []);
const handleLinkClick: MouseEventHandler<HTMLAnchorElement> = (e) => { useEffect(() => {
/** // If inside Saleor Dashboard, redirect to dashboard
* In iframe, link can't be opened in new tab, so Dashboard must be a proxy
*/
if (appBridgeState?.ready) { if (appBridgeState?.ready) {
e.preventDefault(); router.push("/dashboard");
appBridge?.dispatch(
actions.Redirect({
newContext: true,
to: e.currentTarget.href,
})
);
} }
}, [appBridgeState?.ready, router]);
/** if (!mounted) {
* Otherwise, assume app is accessed outside of Dashboard, so href attribute on <a> will work 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 ( return (
<Box padding={8}> <Box padding={8} maxWidth={800} margin="0 auto">
<Text size={11}>Welcome to Saleor App Template (Next.js) 🚀</Text> <Text size={11} as="h1" marginBottom={4}>
<Text as={"p"} marginY={4}> Core Extensions
Saleor App Template is a minimalistic boilerplate that provides a working example of a
Saleor app.
</Text> </Text>
{appBridgeState?.ready && mounted && ( <Text size={6} color="textSecondary" marginBottom={8}>
<Link href="/actions"> Email automation for ManoonOils
<Button variant="secondary">See what your app can do </Button>
</Link>
)}
<Text as={"p"} marginTop={8}>
Explore the App Template by visiting:
</Text> </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> <Box
<a as="div"
onClick={handleLinkClick} backgroundColor="surfaceNeutralPlain"
target="_blank" padding={8}
rel="noreferrer" borderRadius={4}
href="https://github.com/saleor/saleor-app-sdk" marginBottom={8}
> >
<Text color={"info1"}>Saleor App SDK</Text> <Text size={7} marginBottom={4}>
</a> Features
</li> </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> <Box display="flex" flexDirection="column" gap={4}>
<a <Text>
onClick={handleLinkClick} This app connects Saleor to N8N for automated email workflows using Resend.
target="_blank" </Text>
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>
{mounted && !isLocalHost && !appBridgeState?.ready && ( <Box display="flex" gap={4} marginTop={4}>
<> <Button
<Text marginBottom={4} as={"p"}> variant="primary"
Install this app in your Dashboard and get extra powers! onClick={() => {
</Text> window.open("https://dashboard.manoonoils.com/apps", "_blank");
<AddToSaleorForm /> }}
</> >
)} Open in Saleor Dashboard
</Button>
<Button
variant="secondary"
onClick={() => {
router.push("/dashboard");
}}
>
View Dashboard
</Button>
</Box>
</Box>
</Box> </Box>
); );
}; };