@sanity/dashboard
Version:
Tool for rendering dashboard widgets
565 lines (552 loc) • 23.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: !0 });
var jsxRuntime = require("react/jsx-runtime"), react = require("react"), ui = require("@sanity/ui"), styledComponents = require("styled-components"), sanity = require("sanity"), rxjs = require("rxjs"), operators = require("rxjs/operators"), icons = require("@sanity/icons"), imageUrlBuilder = require("@sanity/image-url");
function _interopDefaultCompat(e) {
return e && typeof e == "object" && "default" in e ? e : { default: e };
}
var imageUrlBuilder__default = /* @__PURE__ */ _interopDefaultCompat(imageUrlBuilder);
const Root$3 = styledComponents.styled(ui.Card)`
display: flex;
flex-direction: column;
justify-content: stretch;
height: 100%;
box-sizing: border-box;
position: relative;
`, Header = styledComponents.styled(ui.Card)`
position: sticky;
top: 0;
z-index: 2;
border-top-left-radius: inherit;
border-top-right-radius: inherit;
`, Footer = styledComponents.styled(ui.Card)`
position: sticky;
overflow: hidden;
bottom: 0;
z-index: 2;
border-bottom-right-radius: inherit;
border-bottom-left-radius: inherit;
margin-top: auto;
`, Content = styledComponents.styled(ui.Box)`
position: relative;
z-index: 1;
height: stretch;
min-height: 21.5em;
@media (min-width: ${({ theme }) => theme.sanity.media[0]}px) {
overflow-y: auto;
outline: none;
}
`, DashboardWidgetContainer = react.forwardRef(function(props, ref) {
const { header, children, footer } = props;
return /* @__PURE__ */ jsxRuntime.jsxs(Root$3, { radius: 3, display: "flex", ref, children: [
header && /* @__PURE__ */ jsxRuntime.jsx(Header, { borderBottom: !0, paddingX: 3, paddingY: 4, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 1, textOverflow: "ellipsis", children: header }) }),
children && /* @__PURE__ */ jsxRuntime.jsx(Content, { children }),
footer && /* @__PURE__ */ jsxRuntime.jsx(Footer, { borderTop: !0, children: footer })
] });
});
function useVersionedClient() {
return sanity.useClient({ apiVersion: "2024-08-01" });
}
const DashboardContext = react.createContext({ widgets: [] });
function useDashboardConfig() {
return react.useContext(DashboardContext);
}
function WidgetContainer(props) {
const config = useDashboardConfig(), layout = react.useMemo(
() => ({
...props.layout || {},
...config.layout || {}
}),
[props.layout, config.layout]
);
return /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { shadow: 1, "data-width": layout.width, "data-height": layout.height, children: react.createElement(props.component, {}) });
}
function isUrl(url) {
return url && /^https?:\/\//.test(`${url}`);
}
function getGraphQLUrl(projectId, dataset) {
return `https://${projectId}.api.sanity.io/v1/graphql/${dataset}/default`;
}
function getGroqUrl(projectId, dataset) {
return `https://${projectId}.api.sanity.io/v1/groq/${dataset}`;
}
function getManageUrl(projectId) {
return `https://manage.sanity.io/projects/${projectId}`;
}
const NO_EXPERIMENTAL = [], NO_DATA = [];
function ProjectInfo(props) {
const { __experimental_before = NO_EXPERIMENTAL, data = NO_DATA } = props, [studioApps, setStudioApps] = react.useState(), [graphQLApi, setGraphQLApi] = react.useState(), versionedClient = useVersionedClient(), { projectId = "unknown", dataset = "unknown" } = versionedClient.config();
react.useEffect(() => {
const subscriptions = [];
return subscriptions.push(
versionedClient.observable.request({ uri: "/user-applications", tag: "dashboard.project-info" }).subscribe({
next: (result) => setStudioApps(result.filter((app) => app.type === "studio")),
error: (error) => {
console.error("Error while resolving user applications", error), setStudioApps({
error: "Something went wrong while resolving user applications. See console."
});
}
})
), subscriptions.push(
versionedClient.observable.request({
method: "HEAD",
uri: `/graphql/${dataset}/default`,
tag: "dashboard.project-info.graphql-api"
}).subscribe({
next: () => setGraphQLApi(getGraphQLUrl(projectId, dataset)),
error: (error) => {
error.statusCode === 404 ? setGraphQLApi(void 0) : (console.error("Error while looking for graphQLApi", error), setGraphQLApi({
error: "Something went wrong while looking up graphQLApi. See console."
}));
}
})
), () => {
subscriptions.forEach((s) => s.unsubscribe());
};
}, [dataset, projectId, versionedClient, setGraphQLApi]);
const assembleTableRows = react.useMemo(() => {
let result = [
{
title: "Sanity project",
rows: [
{ title: "Project ID", value: projectId },
{ title: "Dataset", value: dataset }
]
}
];
const apps = data.filter((item) => item.category === "apps");
(Array.isArray(studioApps) ? studioApps : []).forEach((app) => {
apps.push({
title: app.title || "Studio",
value: app.urlType === "internal" ? `https://${app.appHost}.sanity.studio` : app.appHost
});
}), apps.length > 0 && (result = result.concat([{ title: "Apps", rows: apps }])), result = result.concat(
[
{
title: "APIs",
rows: [
{ title: "GROQ", value: getGroqUrl(projectId, dataset) },
{
title: "GraphQL",
value: (typeof graphQLApi == "object" ? "Error" : graphQLApi) ?? "Not deployed"
}
]
}
],
data.filter((item) => item.category === "apis")
);
const otherStuff = {};
return data.forEach((item) => {
item.category && item.category !== "apps" && item.category !== "apis" && (otherStuff[item.category] || (otherStuff[item.category] = []), otherStuff[item.category].push(item));
}), Object.keys(otherStuff).forEach((category) => {
result.push({ title: category, rows: otherStuff[category] });
}), result;
}, [graphQLApi, studioApps, projectId, dataset, data]);
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
__experimental_before.map((widgetConfig, idx) => /* @__PURE__ */ jsxRuntime.jsx(WidgetContainer, { ...widgetConfig }, idx)),
/* @__PURE__ */ jsxRuntime.jsx(ui.Box, { height: "fill", marginTop: __experimental_before?.length > 0 ? 4 : 0, children: /* @__PURE__ */ jsxRuntime.jsx(
DashboardWidgetContainer,
{
footer: /* @__PURE__ */ jsxRuntime.jsx(
ui.Button,
{
style: { width: "100%" },
paddingX: 2,
paddingY: 4,
mode: "bleed",
tone: "primary",
text: "Manage project",
as: "a",
href: getManageUrl(projectId)
}
),
children: /* @__PURE__ */ jsxRuntime.jsx(
ui.Card,
{
paddingY: 4,
radius: 2,
role: "table",
"aria-label": "Project info",
"aria-describedby": "project_info_table",
children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Box, { paddingX: 3, as: "header", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { size: 1, as: "h2", id: "project_info_table", children: "Project info" }) }),
assembleTableRows.map((item) => !item || !item.rows ? null : /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Card, { borderBottom: !0, padding: 3, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 0, muted: !0, role: "columnheader", children: item.title }) }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 4, paddingX: 3, role: "rowgroup", children: item.rows.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Grid, { columns: 2, role: "row", children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "medium", role: "rowheader", children: row.title }),
typeof row.value == "object" && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: row.value?.error }),
typeof row.value == "string" && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: isUrl(row.value) ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, role: "cell", style: { wordBreak: "break-word" }, children: /* @__PURE__ */ jsxRuntime.jsx("a", { href: row.value, children: row.value }) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Code, { size: 1, role: "cell", style: { wordBreak: "break-word" }, children: row.value }) })
] }, `${row.value}-${row.title}`)) })
] }, item.title))
] })
}
)
}
) })
] });
}
function projectInfoWidget(config) {
return {
name: "project-info",
component: ProjectInfo,
layout: config?.layout ?? { width: "medium" }
};
}
const Root$2 = styledComponents.styled(ui.Flex)`
height: ${ui.rem(33)}; // 33 = PREVIEW_SIZES.default.media.height
box-sizing: content-box;
`;
function ProjectUser({ user, isRobot, roles }) {
const listFormat = sanity.useListFormat({ style: "narrow" });
return /* @__PURE__ */ jsxRuntime.jsx(Root$2, { align: "center", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", flex: 1, gap: 2, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: "none", children: isRobot ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 2, children: /* @__PURE__ */ jsxRuntime.jsx(icons.RobotIcon, {}) }) : /* @__PURE__ */ jsxRuntime.jsx(sanity.UserAvatar, { user }) }),
/* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { flex: 1, space: 2, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, style: { color: "inherit" }, textOverflow: "ellipsis", weight: "medium", children: user.displayName }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, size: 1, textOverflow: "ellipsis", children: listFormat.format(roles) })
] })
] }) });
}
function getInviteUrl(projectId) {
return `https://manage.sanity.io/projects/${projectId}/members`;
}
function ProjectUsers() {
const [project, setProject] = react.useState(), [users, setUsers] = react.useState(), [error, setError] = react.useState(), userStore = sanity.useUserStore(), versionedClient = useVersionedClient(), fetchData = react.useCallback(() => {
const { projectId } = versionedClient.config(), subscription = versionedClient.observable.request({
uri: `/projects/${projectId}`,
tag: "dashboard.project-users"
}).pipe(
operators.switchMap(
(_project) => rxjs.from(userStore.getUsers(_project.members.map((mem) => mem.id))).pipe(
operators.map((_users) => ({ project: _project, users: _users }))
)
)
).subscribe({
next: ({ users: _users, project: _project }) => {
setProject(_project), setUsers(
(Array.isArray(_users) ? _users : [_users]).sort(
(userA, userB) => sortUsersByRobotStatus(userA, userB, _project)
)
);
},
error: (e) => setError(e)
});
return () => subscription.unsubscribe();
}, [userStore, versionedClient]);
react.useEffect(() => fetchData(), [fetchData]);
const handleRetryFetch = react.useCallback(() => fetchData(), [fetchData]), isLoading = !users || !project;
return error ? /* @__PURE__ */ jsxRuntime.jsx(DashboardWidgetContainer, { header: "Project users", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { children: [
"Something went wrong while fetching data. You could",
" ",
/* @__PURE__ */ jsxRuntime.jsx("a", { onClick: handleRetryFetch, title: "Retry users fetch", style: { cursor: "pointer" }, children: "retry" }),
"..?"
] }) }) }) : /* @__PURE__ */ jsxRuntime.jsxs(
DashboardWidgetContainer,
{
header: "Project users",
footer: /* @__PURE__ */ jsxRuntime.jsx(
ui.Button,
{
style: { width: "100%" },
paddingX: 2,
paddingY: 4,
mode: "bleed",
tone: "primary",
text: "Manage members",
as: "a",
loading: isLoading,
href: isLoading ? void 0 : getInviteUrl(project.id)
}
),
children: [
isLoading && /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { paddingY: 5, paddingX: 2, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { align: "center", muted: !0, size: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {}) }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { align: "center", size: 1, muted: !0, children: "Loading items\u2026" })
] }) }),
!isLoading && /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, padding: 3, children: users?.map((user) => {
const membership = project.members.find((member) => member.id === user.id);
return /* @__PURE__ */ jsxRuntime.jsx(
ProjectUser,
{
user,
isRobot: membership?.isRobot ?? !1,
roles: membership?.roles.map((role) => role.title) || []
},
user.id
);
}) })
]
}
);
}
function sortUsersByRobotStatus(userA, userB, project) {
const { members } = project, membershipA = members.find((member) => member.id === userA?.id), membershipB = members.find((member) => member.id === userB?.id);
return membershipA?.isRobot === membershipB?.isRobot ? (membershipA?.createdAt || "") > (membershipB?.createdAt || "") ? 1 : -1 : membershipA?.isRobot ? 1 : -1;
}
function projectUsersWidget(config) {
return {
name: "project-info",
component: ProjectUsers,
layout: config?.layout
};
}
const PlayIconBox = styledComponents.styled(ui.Box)`
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
&:before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2.75em;
height: 2.75em;
border-radius: 50%;
background: ${({ theme }) => theme.sanity.color.card.enabled.bg};
opacity: 0.75;
}
`, Root$1 = styledComponents.styled(ui.Flex)`
&:hover {
${PlayIconBox} {
&:before {
opacity: 1;
}
}
}
`, PosterCard = styledComponents.styled(ui.Card)`
width: 100%;
padding-bottom: calc(9 / 16 * 100%);
position: relative;
`, Poster = styledComponents.styled.img`
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
object-fit: cover;
display: block;
&:not([src]) {
display: none;
}
`;
function Tutorial(props) {
const { title, posterURL, showPlayIcon, href, presenterName, presenterSubtitle } = props;
return /* @__PURE__ */ jsxRuntime.jsx(Root$1, { flex: 1, children: /* @__PURE__ */ jsxRuntime.jsx(
ui.Card,
{
sizing: "border",
flex: 1,
padding: 2,
radius: 2,
as: "a",
href,
target: "_blank",
rel: "noopener noreferrer",
style: { position: "relative" },
children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { direction: "column", style: { height: "100%" }, children: [
posterURL && /* @__PURE__ */ jsxRuntime.jsxs(PosterCard, { marginBottom: 1, children: [
/* @__PURE__ */ jsxRuntime.jsx(Poster, { src: posterURL }),
showPlayIcon && /* @__PURE__ */ jsxRuntime.jsx(PlayIconBox, { display: "flex", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { align: "center", children: /* @__PURE__ */ jsxRuntime.jsx(icons.PlayIcon, {}) }) })
] }),
/* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { direction: "column", justify: "space-between", paddingY: 2, flex: 1, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { as: "h3", size: 1, children: title }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Box, { marginTop: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, flex: 1, children: [
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: presenterName }),
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 0, style: { opacity: 0.7 }, children: presenterSubtitle })
] }) })
] })
] })
}
) });
}
const tutorialsProjectConfig = {
projectId: "3do82whm",
dataset: "next"
};
function useDataAdapter() {
const versionedClient = useVersionedClient();
return react.useMemo(
() => ({
getFeed: (templateRepoId) => {
const uri = templateRepoId ? `/addons/dashboard?templateRepoId=${templateRepoId}` : "/addons/dashboard";
return versionedClient.observable.request({
uri,
tag: "dashboard.sanity-tutorials",
withCredentials: !1
});
},
urlBuilder: imageUrlBuilder__default.default(tutorialsProjectConfig)
}),
[versionedClient]
);
}
function createUrl(slug, type) {
return type === "tutorial" ? `https://www.sanity.io/docs/tutorials/${slug.current}` : type === "guide" ? `https://www.sanity.io/docs/guides/${slug.current}` : !1;
}
function SanityTutorials(props) {
const { templateRepoId } = props, [feedItems, setFeedItems] = react.useState([]), { getFeed, urlBuilder } = useDataAdapter();
return react.useEffect(() => {
const subscription = getFeed(templateRepoId).subscribe((response) => {
setFeedItems(response.items);
});
return () => {
subscription.unsubscribe();
};
}, [setFeedItems, getFeed, templateRepoId]), /* @__PURE__ */ jsxRuntime.jsx(DashboardWidgetContainer, { header: "Learn about Sanity", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { as: "ul", overflow: "auto", align: "stretch", paddingY: 2, children: feedItems?.map((feedItem, index) => {
if (!feedItem.title || !feedItem.guideOrTutorial && !feedItem.externalLink)
return null;
const presenter = feedItem.presenter || feedItem.guideOrTutorial?.presenter || {}, subtitle = feedItem.category, { guideOrTutorial = {} } = feedItem, href = (guideOrTutorial.slug ? createUrl(guideOrTutorial.slug, guideOrTutorial._type) : feedItem.externalLink) || feedItem.externalLink;
return /* @__PURE__ */ jsxRuntime.jsx(
ui.Flex,
{
as: "li",
paddingRight: index < feedItems?.length - 1 ? 1 : 3,
paddingLeft: index === 0 ? 3 : 0,
align: "stretch",
style: { minWidth: 272, width: "30%" },
children: /* @__PURE__ */ jsxRuntime.jsx(
Tutorial,
{
title: feedItem.title,
href: href ?? "",
presenterName: presenter.name,
presenterSubtitle: subtitle,
showPlayIcon: feedItem.hasVideo,
posterURL: feedItem.poster ? urlBuilder.image(feedItem.poster).height(360).url() : void 0
}
)
},
feedItem._id
);
}) }) });
}
function sanityTutorialsWidget(config) {
return {
name: "sanity-tutorials",
component: SanityTutorials,
layout: config?.layout ?? { width: "full" }
};
}
function DashboardLayout(props) {
return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { width: 4, padding: 4, sizing: "border", style: { height: "100%", overflowY: "auto" }, children: props.children });
}
const media = {
small: (...args) => styledComponents.css`
@media (min-width: ${({ theme }) => theme.sanity.media[0]}px) {
${styledComponents.css(...args)}
}
`,
medium: (...args) => styledComponents.css`
@media (min-width: ${({ theme }) => theme.sanity.media[2]}px) {
${styledComponents.css(...args)}
}
`
}, Root = styledComponents.styled(ui.Grid)`
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
& > div {
overflow: hidden;
}
& > div[data-width='medium'] {
${media.small`
grid-column: span 2;
`}
}
& > div[data-width='large'] {
${media.small`
grid-column: span 2;
`}
${media.medium`
grid-column: span 3;
`}
}
& > div[data-width='full'] {
${media.small`
grid-column: 1 / -1;
`}
}
& > div[data-height='medium'] {
${media.small`
grid-row: span 2;
`}
}
& > div[data-height='large'] {
${media.small`
grid-row: span 2;
`}
${media.medium`
grid-row: span 3;
`}
}
& > div[data-height='full'] {
${media.medium`
grid-row: 1 / -1;
`}
}
`, NO_WIDGETS = [], NO_LAYOUT = {};
function WidgetGroup(props) {
const {
config: { layout = NO_LAYOUT, widgets = NO_WIDGETS }
} = props;
return /* @__PURE__ */ jsxRuntime.jsxs(
Root,
{
autoFlow: "row dense",
"data-width": layout.width || "auto",
"data-height": layout.height || "auto",
gap: 4,
children: [
widgets.length ? null : /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 4, shadow: 1, tone: "primary", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { align: "center", children: "Add some widgets to populate this space." }) }),
widgets.map((widgetConfig, index) => widgetConfig.type === "__experimental_group" ? /* @__PURE__ */ jsxRuntime.jsx(WidgetGroup, { config: widgetConfig }, index) : widgetConfig.component ? /* @__PURE__ */ jsxRuntime.jsx(WidgetContainer, { ...widgetConfig }, index) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Box, { children: [
widgetConfig.name,
" is missing widget component"
] }, index))
]
}
);
}
function Dashboard({ config }) {
return config ? /* @__PURE__ */ jsxRuntime.jsx(DashboardContext.Provider, { value: config, children: /* @__PURE__ */ jsxRuntime.jsx(DashboardLayout, { children: /* @__PURE__ */ jsxRuntime.jsx(WidgetGroup, { config }) }) }) : null;
}
const strokeStyle = {
stroke: "currentColor",
strokeWidth: 1.2
}, DashboardIcon = () => /* @__PURE__ */ jsxRuntime.jsxs(
"svg",
{
"data-sanity-icon": !0,
viewBox: "0 0 25 25",
fill: "none",
xmlns: "http://www.w3.org/2000/svg",
preserveAspectRatio: "xMidYMid",
width: "1em",
height: "1em",
children: [
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19.5 19.5H5.5V5.5H19.5V19.5Z", style: strokeStyle }),
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5.5 12.5H19.5", style: strokeStyle }),
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14.5 19.5V12.5M10.5 12.5V5.5", style: strokeStyle })
]
}
), dashboardTool = sanity.definePlugin((config = {}) => {
const pluginConfig = {
layout: config.defaultLayout ?? {},
widgets: config.widgets ?? []
}, title = config.title ?? "Dashboard", name = config.name ?? "dashboard", icon = config.icon ?? DashboardIcon;
return {
name: "dashboard",
tools: (prev, context) => [
...prev,
{
title,
name,
icon,
component: () => /* @__PURE__ */ jsxRuntime.jsx(Dashboard, { config: pluginConfig })
}
]
};
});
exports.DashboardWidgetContainer = DashboardWidgetContainer;
exports.dashboardTool = dashboardTool;
exports.projectInfoWidget = projectInfoWidget;
exports.projectUsersWidget = projectUsersWidget;
exports.sanityTutorialsWidget = sanityTutorialsWidget;
//# sourceMappingURL=index.js.map