@sanity/dashboard 2.30.2-shopify.2 → 3.0.0-studio-v3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +123 -44
  2. package/lib/cjs/index.js +1005 -0
  3. package/lib/cjs/index.js.map +1 -0
  4. package/lib/esm/index.js +980 -0
  5. package/lib/esm/index.js.map +1 -0
  6. package/lib/types/index.d.ts +42 -0
  7. package/lib/types/index.d.ts.map +1 -0
  8. package/package.json +63 -16
  9. package/sanity.json +2 -53
  10. package/src/components/DashboardLayout.tsx +10 -0
  11. package/src/components/DashboardWidgetContainer.tsx +69 -0
  12. package/src/components/NotFoundWidget.tsx +30 -0
  13. package/src/components/WidgetGroup.tsx +108 -0
  14. package/src/containers/Dashboard.tsx +19 -0
  15. package/src/containers/DashboardContext.tsx +8 -0
  16. package/src/containers/WidgetContainer.tsx +21 -0
  17. package/src/index.tsx +62 -0
  18. package/src/types.ts +21 -0
  19. package/src/versionedClient.ts +7 -0
  20. package/src/widgets/projectInfo/ProjectInfo.tsx +233 -0
  21. package/src/widgets/projectInfo/index.ts +10 -0
  22. package/src/widgets/projectUsers/ProjectUsers.tsx +170 -0
  23. package/src/widgets/projectUsers/index.ts +10 -0
  24. package/src/widgets/sanityTutorials/SanityTutorials.tsx +77 -0
  25. package/src/widgets/sanityTutorials/Tutorial.tsx +111 -0
  26. package/src/widgets/sanityTutorials/dataAdapter.ts +48 -0
  27. package/src/widgets/sanityTutorials/index.ts +10 -0
  28. package/v2-incompatible.js +11 -0
  29. package/.babelrc +0 -4
  30. package/lib/DashboardTool.js +0 -59
  31. package/lib/components/DashboardLayout.js +0 -35
  32. package/lib/components/NotFoundWidget.js +0 -51
  33. package/lib/components/WidgetGroup.js +0 -67
  34. package/lib/components/dashboardWidget.js +0 -51
  35. package/lib/containers/Dashboard.js +0 -32
  36. package/lib/containers/WidgetContainer.js +0 -56
  37. package/lib/dashboardConfig.js +0 -16
  38. package/lib/legacyParts.js +0 -55
  39. package/lib/versionedClient.js +0 -20
  40. package/lib/widget.css +0 -62
  41. package/lib/widgets/projectInfo/ProjectInfo.js +0 -265
  42. package/lib/widgets/projectInfo/index.js +0 -19
  43. package/lib/widgets/projectUsers/ProjectUsers.js +0 -188
  44. package/lib/widgets/projectUsers/index.js +0 -16
  45. package/lib/widgets/sanityTutorials/SanityTutorials.js +0 -115
  46. package/lib/widgets/sanityTutorials/Tutorial.js +0 -111
  47. package/lib/widgets/sanityTutorials/dataAdapter.js +0 -28
  48. package/lib/widgets/sanityTutorials/index.js +0 -19
  49. package/tsconfig.json +0 -17
package/src/index.tsx ADDED
@@ -0,0 +1,62 @@
1
+ import React, {CSSProperties} from 'react'
2
+ import {Dashboard} from './containers/Dashboard'
3
+ import {createPlugin} from 'sanity'
4
+ import {DashboardConfig, DashboardWidget, LayoutConfig} from './types'
5
+
6
+ const strokeStyle: CSSProperties = {
7
+ stroke: 'currentColor',
8
+ strokeWidth: 1.2,
9
+ }
10
+
11
+ const DashboardIcon = () => (
12
+ <svg
13
+ data-sanity-icon
14
+ viewBox="0 0 25 25"
15
+ fill="none"
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ preserveAspectRatio="xMidYMid"
18
+ width="1em"
19
+ height="1em"
20
+ >
21
+ <path d="M19.5 19.5H5.5V5.5H19.5V19.5Z" style={strokeStyle} />
22
+ <path d="M5.5 12.5H19.5" style={strokeStyle} />
23
+ <path d="M14.5 19.5V12.5M10.5 12.5V5.5" style={strokeStyle} />
24
+ </svg>
25
+ )
26
+
27
+ export * from './types'
28
+ export * from './components/DashboardWidgetContainer'
29
+ export * from './widgets/projectInfo'
30
+ export * from './widgets/projectUsers'
31
+ export * from './widgets/sanityTutorials'
32
+
33
+ export interface DashboardPluginConfig {
34
+ widgets?: DashboardWidget[]
35
+
36
+ /**
37
+ * Will be used for widgets that do not define a layout directly.
38
+ */
39
+ defaultLayout?: LayoutConfig
40
+ }
41
+
42
+ export const dashboardTool = createPlugin<DashboardPluginConfig>((config = {}) => {
43
+ const pluginConfig: DashboardConfig = {
44
+ layout: config.defaultLayout ?? {},
45
+ widgets: config.widgets ?? [],
46
+ }
47
+
48
+ return {
49
+ name: 'dashboard',
50
+ tools: (prev, context) => {
51
+ return [
52
+ ...prev,
53
+ {
54
+ title: 'Dashboard',
55
+ name: 'dashboard',
56
+ icon: DashboardIcon,
57
+ component: () => <Dashboard config={pluginConfig} />,
58
+ },
59
+ ]
60
+ },
61
+ }
62
+ })
package/src/types.ts ADDED
@@ -0,0 +1,21 @@
1
+ import {ComponentClass, FunctionComponent} from 'react'
2
+
3
+ export interface DashboardWidget {
4
+ name: string
5
+ type?: '__experimental_group'
6
+ component: FunctionComponent<any> | ComponentClass<any>
7
+ layout?: LayoutConfig
8
+ widgets?: DashboardWidget[]
9
+ }
10
+
11
+ export type LayoutSize = 'auto' | 'small' | 'medium' | 'large' | 'full'
12
+
13
+ export interface LayoutConfig {
14
+ width?: LayoutSize
15
+ height?: LayoutSize
16
+ }
17
+
18
+ export interface DashboardConfig {
19
+ widgets: DashboardWidget[]
20
+ layout?: LayoutConfig
21
+ }
@@ -0,0 +1,7 @@
1
+ import {useClient} from 'sanity'
2
+ import {useMemo} from 'react'
3
+
4
+ export function useVersionedClient() {
5
+ const client = useClient()
6
+ return useMemo(() => client.withConfig({apiVersion: '1'}), [client])
7
+ }
@@ -0,0 +1,233 @@
1
+ import React, {useEffect, useMemo, useState} from 'react'
2
+ import {Box, Card, Stack, Heading, Grid, Label, Text, Code, Button} from '@sanity/ui'
3
+ import {useVersionedClient} from '../../versionedClient'
4
+ import {Subscription} from 'rxjs'
5
+ import {WidgetContainer} from '../../containers/WidgetContainer'
6
+ import {DashboardWidgetContainer} from '../../components/DashboardWidgetContainer'
7
+ import {DashboardWidget} from '../../types'
8
+
9
+ export interface ProjectInfoProps {
10
+ __experimental_before?: DashboardWidget[]
11
+ data: ProjectData[]
12
+ }
13
+
14
+ interface App {
15
+ title: string
16
+ rows?: App[]
17
+ value?: string | {error: string}
18
+ }
19
+
20
+ interface ProjectData {
21
+ title: string
22
+ category?: string
23
+ }
24
+
25
+ function isUrl(url?: string) {
26
+ return url && /^https?:\/\//.test(`${url}`)
27
+ }
28
+
29
+ function getGraphQlUrl(projectId: string, dataset: string) {
30
+ return `https://${projectId}.api.sanity.io/v1/graphql/${dataset}/default`
31
+ }
32
+
33
+ function getGroqUrl(projectId: string, dataset: string) {
34
+ return `https://${projectId}.api.sanity.io/v1/groq/${dataset}`
35
+ }
36
+
37
+ function getManageUrl(projectId: string) {
38
+ return `https://manage.sanity.io/projects/${projectId}`
39
+ }
40
+
41
+ const NO_EXPERIMENTAL: DashboardWidget[] = []
42
+ const NO_DATA: ProjectData[] = []
43
+
44
+ export function ProjectInfo(props: ProjectInfoProps) {
45
+ const {__experimental_before = NO_EXPERIMENTAL, data = NO_DATA} = props
46
+ const [studioHost, setStudioHost] = useState<string | {error: string} | undefined>()
47
+ const [graphqlApi, setGraphQlApi] = useState<string | {error: string} | undefined>()
48
+ const versionedClient = useVersionedClient()
49
+ const {projectId = 'unknown', dataset = 'unknown'} = versionedClient.config()
50
+
51
+ useEffect(() => {
52
+ const subscriptions: Subscription[] = []
53
+
54
+ subscriptions.push(
55
+ versionedClient.observable
56
+ .request<{studioHost: string}>({uri: `/projects/${projectId}`})
57
+ .subscribe({
58
+ next: (result) => {
59
+ const {studioHost: host} = result
60
+ setStudioHost(host ? `https://${host}.sanity.studio` : undefined)
61
+ },
62
+ error: (error) => {
63
+ console.error('Error while looking for studioHost', error)
64
+ setStudioHost({
65
+ error: 'Something went wrong while looking up studioHost. See console.',
66
+ })
67
+ },
68
+ })
69
+ )
70
+
71
+ // ping assumed graphql endpoint
72
+ subscriptions.push(
73
+ versionedClient.observable
74
+ .request({
75
+ method: 'HEAD',
76
+ uri: `/graphql/${dataset}/default`,
77
+ })
78
+ .subscribe({
79
+ next: () => setGraphQlApi(getGraphQlUrl(projectId, dataset)),
80
+ error: (error) => {
81
+ if (error.statusCode === 404) {
82
+ setGraphQlApi(undefined)
83
+ } else {
84
+ console.error('Error while looking for graphqlApi', error)
85
+ setGraphQlApi({
86
+ error: 'Something went wrong while looking up graphqlApi. See console.',
87
+ })
88
+ }
89
+ },
90
+ })
91
+ )
92
+
93
+ return () => {
94
+ subscriptions.forEach((s) => s.unsubscribe())
95
+ }
96
+ }, [dataset, projectId, versionedClient, setGraphQlApi, setStudioHost])
97
+
98
+ const assembleTableRows = useMemo(() => {
99
+ let result: App[] = [
100
+ {
101
+ title: 'Sanity project',
102
+ rows: [
103
+ {title: 'Project ID', value: projectId},
104
+ {title: 'Dataset', value: dataset},
105
+ ],
106
+ },
107
+ ]
108
+
109
+ // Handle any apps
110
+ const apps: App[] = [
111
+ studioHost ? {title: 'Studio', value: studioHost} : null,
112
+ ...data.filter((item) => item.category === 'apps'),
113
+ ].filter((a): a is App => !!a)
114
+ if (apps.length > 0) {
115
+ result = result.concat([{title: 'Apps', rows: apps}])
116
+ }
117
+
118
+ // Handle APIs
119
+ result = result.concat(
120
+ [
121
+ {
122
+ title: 'APIs',
123
+ rows: [
124
+ {title: 'GROQ', value: getGroqUrl(projectId, dataset)},
125
+ {
126
+ title: 'GraphQL',
127
+ value: (typeof graphqlApi === 'object' ? 'Error' : graphqlApi) ?? 'Not deployed',
128
+ },
129
+ ],
130
+ },
131
+ ],
132
+ data.filter((item) => item.category === 'apis')
133
+ )
134
+
135
+ // Handle whatever else there might be
136
+ const otherStuff: Record<string, ProjectData[]> = {}
137
+ data.forEach((item) => {
138
+ if (item.category && item.category !== 'apps' && item.category !== 'apis') {
139
+ if (!otherStuff[item.category]) {
140
+ otherStuff[item.category] = []
141
+ }
142
+ otherStuff[item.category].push(item)
143
+ }
144
+ })
145
+ Object.keys(otherStuff).forEach((category) => {
146
+ result.push({title: category, rows: otherStuff[category]})
147
+ })
148
+
149
+ return result
150
+ }, [graphqlApi, studioHost, projectId, dataset, data])
151
+
152
+ return (
153
+ <>
154
+ {__experimental_before.map((widgetConfig, idx) => (
155
+ <WidgetContainer key={idx} {...widgetConfig} />
156
+ ))}
157
+ <Box height="fill" marginTop={__experimental_before?.length > 0 ? 4 : 0}>
158
+ <DashboardWidgetContainer
159
+ footer={
160
+ <Button
161
+ style={{width: '100%'}}
162
+ paddingX={2}
163
+ paddingY={4}
164
+ mode="bleed"
165
+ tone="primary"
166
+ text="Manage project"
167
+ as="a"
168
+ href={getManageUrl(projectId)}
169
+ />
170
+ }
171
+ >
172
+ <Card
173
+ paddingY={4}
174
+ radius={2}
175
+ role="table"
176
+ aria-label="Project info"
177
+ aria-describedby="project_info_table"
178
+ >
179
+ <Stack space={4}>
180
+ <Box paddingX={3} as="header">
181
+ <Heading size={1} as="h2" id="project_info_table">
182
+ Project info
183
+ </Heading>
184
+ </Box>
185
+ {assembleTableRows.map((item) => {
186
+ if (!item || !item.rows) {
187
+ return null
188
+ }
189
+
190
+ return (
191
+ <Stack key={item.title} space={3}>
192
+ <Card borderBottom padding={3}>
193
+ <Label size={0} muted role="columnheader">
194
+ {item.title}
195
+ </Label>
196
+ </Card>
197
+ <Stack space={4} paddingX={3} role="rowgroup">
198
+ {item.rows.map((row) => {
199
+ return (
200
+ <Grid key={row.title} columns={2} role="row">
201
+ <Text weight="medium" role="rowheader">
202
+ {row.title}
203
+ </Text>
204
+ {typeof row.value === 'object' && (
205
+ <Text size={1}>{row.value?.error}</Text>
206
+ )}
207
+ {typeof row.value === 'string' && (
208
+ <>
209
+ {isUrl(row.value) ? (
210
+ <Text size={1} role="cell" style={{wordBreak: 'break-word'}}>
211
+ <a href={row.value}>{row.value}</a>
212
+ </Text>
213
+ ) : (
214
+ <Code size={1} role="cell" style={{wordBreak: 'break-word'}}>
215
+ {row.value}
216
+ </Code>
217
+ )}
218
+ </>
219
+ )}
220
+ </Grid>
221
+ )
222
+ })}
223
+ </Stack>
224
+ </Stack>
225
+ )
226
+ })}
227
+ </Stack>
228
+ </Card>
229
+ </DashboardWidgetContainer>
230
+ </Box>
231
+ </>
232
+ )
233
+ }
@@ -0,0 +1,10 @@
1
+ import {ProjectInfo} from './ProjectInfo'
2
+ import {LayoutConfig, DashboardWidget} from '../../types'
3
+
4
+ export function projectInfoWidget(config?: {layout?: LayoutConfig}): DashboardWidget {
5
+ return {
6
+ name: 'project-info',
7
+ component: ProjectInfo,
8
+ layout: config?.layout ?? {width: 'medium'},
9
+ }
10
+ }
@@ -0,0 +1,170 @@
1
+ import React, {useCallback, useEffect, useState} from 'react'
2
+ import {from} from 'rxjs'
3
+ import {map, switchMap} from 'rxjs/operators'
4
+ import {Stack, Spinner, Card, Box, Text, Button} from '@sanity/ui'
5
+ import {RobotIcon} from '@sanity/icons'
6
+ import styled from 'styled-components'
7
+ import {DefaultPreview} from 'sanity/_unstable'
8
+ import {useVersionedClient} from '../../versionedClient'
9
+ import {User} from 'sanity'
10
+ import {DashboardWidgetContainer} from '../../components/DashboardWidgetContainer'
11
+ import {useUserStore} from 'sanity/lib/dts/src/datastores'
12
+
13
+ const AvatarWrapper = styled(Card)`
14
+ box-sizing: border-box;
15
+ border-radius: 50%;
16
+ border-color: transparent;
17
+ overflow: hidden;
18
+ width: 100%;
19
+ height: 100%;
20
+
21
+ & > img {
22
+ width: 100%;
23
+ height: auto;
24
+ }
25
+ `
26
+
27
+ function getInviteUrl(projectId: string) {
28
+ return `https://manage.sanity.io/projects/${projectId}/team/invite`
29
+ }
30
+
31
+ interface Member {
32
+ id: string
33
+ role: string
34
+ isRobot: boolean
35
+ }
36
+
37
+ interface Project {
38
+ id: string
39
+ members: Member[]
40
+ }
41
+
42
+ export function ProjectUsers() {
43
+ const [project, setProject] = useState<Project | undefined>()
44
+ const [users, setUsers] = useState<User[] | undefined>()
45
+ const [error, setError] = useState<Error | undefined>()
46
+
47
+ const userStore = useUserStore()
48
+ const versionedClient = useVersionedClient()
49
+
50
+ const fetchData = useCallback(() => {
51
+ const {projectId} = versionedClient.config()
52
+ const subscription = versionedClient.observable
53
+ .request<Project>({
54
+ uri: `/projects/${projectId}`,
55
+ })
56
+ .pipe(
57
+ switchMap((_project) =>
58
+ from(userStore.getUsers(_project.members.map((mem) => mem.id))).pipe(
59
+ map((_users) => ({project: _project, users: _users}))
60
+ )
61
+ )
62
+ )
63
+ .subscribe({
64
+ next: ({users: _users, project: _project}) => {
65
+ setProject(_project)
66
+ setUsers(
67
+ (Array.isArray(_users) ? _users : [_users]).sort((userA, userB) =>
68
+ sortUsersByRobotStatus(userA, userB, _project)
69
+ )
70
+ )
71
+ },
72
+ error: (e: Error) => setError(e),
73
+ })
74
+
75
+ return () => subscription.unsubscribe()
76
+ }, [userStore, versionedClient])
77
+
78
+ useEffect(() => fetchData(), [fetchData])
79
+
80
+ const handleRetryFetch = useCallback(() => fetchData(), [fetchData])
81
+
82
+ const isLoading = !users || !project
83
+
84
+ if (error) {
85
+ return (
86
+ <DashboardWidgetContainer header="Project users">
87
+ <Box padding={4}>
88
+ <Text>
89
+ Something went wrong while fetching data. You could{' '}
90
+ <a onClick={handleRetryFetch} title="Retry users fetch" style={{cursor: 'pointer'}}>
91
+ retry
92
+ </a>
93
+ ..?
94
+ </Text>
95
+ </Box>
96
+ </DashboardWidgetContainer>
97
+ )
98
+ }
99
+
100
+ return (
101
+ <DashboardWidgetContainer
102
+ header="Project users"
103
+ footer={
104
+ <Button
105
+ style={{width: '100%'}}
106
+ paddingX={2}
107
+ paddingY={4}
108
+ mode="bleed"
109
+ tone="primary"
110
+ text="Invite members"
111
+ as="a"
112
+ loading={isLoading}
113
+ href={isLoading ? undefined : getInviteUrl(project.id)}
114
+ />
115
+ }
116
+ >
117
+ {isLoading && (
118
+ <Box paddingY={5} paddingX={2}>
119
+ <Stack space={4}>
120
+ <Text align="center" muted size={1}>
121
+ <Spinner />
122
+ </Text>
123
+ <Text align="center" size={1} muted>
124
+ Loading items...
125
+ </Text>
126
+ </Stack>
127
+ </Box>
128
+ )}
129
+
130
+ {!isLoading && (
131
+ <Stack space={3} padding={3}>
132
+ {users?.map((user) => {
133
+ const membership = project.members.find((member) => member.id === user.id)
134
+ const media = membership?.isRobot ? (
135
+ <Text size={3}>
136
+ <RobotIcon />
137
+ </Text>
138
+ ) : (
139
+ <AvatarWrapper tone="transparent">
140
+ {user?.imageUrl && <img src={user.imageUrl} alt={user?.displayName} />}
141
+ </AvatarWrapper>
142
+ )
143
+ return (
144
+ <Box key={user.id}>
145
+ <DefaultPreview
146
+ title={user.displayName}
147
+ subtitle={membership?.role}
148
+ media={media}
149
+ />
150
+ </Box>
151
+ )
152
+ })}
153
+ </Stack>
154
+ )}
155
+ </DashboardWidgetContainer>
156
+ )
157
+ }
158
+
159
+ function sortUsersByRobotStatus(userA: User, userB: User, project: Project) {
160
+ const {members} = project
161
+ const membershipA = members.find((member) => member.id === userA?.id)
162
+ const membershipB = members.find((member) => member.id === userB?.id)
163
+ if (membershipA?.isRobot) {
164
+ return 1
165
+ }
166
+ if (membershipB?.isRobot) {
167
+ return -1
168
+ }
169
+ return 0
170
+ }
@@ -0,0 +1,10 @@
1
+ import {ProjectUsers} from './ProjectUsers'
2
+ import {LayoutConfig, DashboardWidget} from '../../types'
3
+
4
+ export function projectUsersWidget(config?: {layout?: LayoutConfig}): DashboardWidget {
5
+ return {
6
+ name: 'project-info',
7
+ component: ProjectUsers,
8
+ layout: config?.layout,
9
+ }
10
+ }
@@ -0,0 +1,77 @@
1
+ import React, {useEffect, useState} from 'react'
2
+ import {Flex} from '@sanity/ui'
3
+ import {Tutorial} from './Tutorial'
4
+ import {FeedItem, Guide, useDataAdapter} from './dataAdapter'
5
+ import {DashboardWidgetContainer} from '../../components/DashboardWidgetContainer'
6
+
7
+ function createUrl(slug: {current: string}, type?: string) {
8
+ if (type === 'tutorial') {
9
+ return `https://www.sanity.io/docs/tutorials/${slug.current}`
10
+ } else if (type === 'guide') {
11
+ return `https://www.sanity.io/docs/guides/${slug.current}`
12
+ }
13
+ return false
14
+ }
15
+
16
+ export interface SanityTutorialsProps {
17
+ templateRepoId: string
18
+ }
19
+
20
+ export function SanityTutorials(props: SanityTutorialsProps) {
21
+ const {templateRepoId} = props
22
+ const [feedItems, setFeedItems] = useState<FeedItem[]>([])
23
+
24
+ const {getFeed, urlBuilder} = useDataAdapter()
25
+
26
+ useEffect(() => {
27
+ const subscription = getFeed(templateRepoId).subscribe((response) => {
28
+ setFeedItems(response.items)
29
+ })
30
+ return () => {
31
+ subscription.unsubscribe()
32
+ }
33
+ }, [setFeedItems, getFeed, templateRepoId])
34
+
35
+ const title = 'Learn about Sanity'
36
+
37
+ return (
38
+ <DashboardWidgetContainer header={title}>
39
+ <Flex as="ul" overflow="auto" align="stretch" paddingY={2}>
40
+ {feedItems?.map((feedItem, index) => {
41
+ if (!feedItem.title || (!feedItem.guideOrTutorial && !feedItem.externalLink)) {
42
+ return null
43
+ }
44
+ const presenter = feedItem.presenter || feedItem.guideOrTutorial?.presenter || {}
45
+ const subtitle = feedItem.category
46
+ const {guideOrTutorial = {} as Guide} = feedItem
47
+ const href =
48
+ (guideOrTutorial.slug
49
+ ? createUrl(guideOrTutorial.slug, guideOrTutorial._type)
50
+ : feedItem.externalLink) || feedItem.externalLink
51
+
52
+ return (
53
+ <Flex
54
+ as="li"
55
+ key={feedItem._id}
56
+ paddingRight={index < feedItems?.length - 1 ? 1 : 3}
57
+ paddingLeft={index === 0 ? 3 : 0}
58
+ align="stretch"
59
+ style={{minWidth: 272, width: '30%'}}
60
+ >
61
+ <Tutorial
62
+ title={feedItem.title}
63
+ href={href ?? ''}
64
+ presenterName={presenter.name}
65
+ presenterSubtitle={subtitle}
66
+ showPlayIcon={feedItem.hasVideo}
67
+ posterURL={
68
+ feedItem.poster ? urlBuilder.image(feedItem.poster).height(360).url() : undefined
69
+ }
70
+ />
71
+ </Flex>
72
+ )
73
+ })}
74
+ </Flex>
75
+ </DashboardWidgetContainer>
76
+ )
77
+ }