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 { 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>
);
};