UNPKG

@sanity/dashboard

Version:

Tool for rendering dashboard widgets

565 lines (552 loc) 23.2 kB
"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