Skip to content

Commit f381d33

Browse files
authored
Merge pull request #1774 from RedisInsight/fe/feature/RI-4186-custom-tutorials
#RI-4186 - initial fe implementation for custom tutorials
2 parents 1ad8fac + 2fd0c3b commit f381d33

38 files changed

+957
-201
lines changed

redisinsight/ui/src/components/uploadFile/UploadFile.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { EuiButtonEmpty, EuiText } from '@elastic/eui'
44
import styles from './styles.module.scss'
55

66
export interface Props {
7-
onFileChange: ({ target: { files } }: { target: { files: FileList | null } }) => void
8-
onClick: () => void
7+
onFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void
8+
onClick?: () => void
9+
accept?: string
910
}
1011

11-
const UploadFile = ({ onFileChange, onClick }: Props) => (
12+
const UploadFile = ({ onFileChange, onClick, accept }: Props) => (
1213
<EuiButtonEmpty
1314
iconType="folderOpen"
1415
className={styles.emptyBtn}
@@ -19,9 +20,9 @@ const UploadFile = ({ onFileChange, onClick }: Props) => (
1920
type="file"
2021
id="upload-input-file"
2122
data-testid="upload-input-file"
22-
accept="application/json, text/plain"
23+
accept={accept || '*'}
2324
onChange={onFileChange}
24-
onClick={onClick}
25+
onClick={() => onClick?.()}
2526
className={styles.fileDrop}
2627
aria-label="Select file"
2728
/>

redisinsight/ui/src/constants/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,14 @@ enum ApiEndpoints {
8383
GUIDES = 'static/guides/guides.json',
8484
// TODO double check it, when tutorials will be completed
8585
TUTORIALS = 'static/tutorials/tutorials.json',
86+
CUSTOM_TUTORIALS = 'custom-tutorials',
87+
CUSTOM_TUTORIALS_MANIFEST = 'custom-tutorials/manifest',
8688
PLUGINS = 'plugins',
8789
STATE = 'state',
8890
CONTENT_CREATE_DATABASE = 'static/content/create-redis.json',
8991
GUIDES_PATH = 'static/guides',
9092
TUTORIALS_PATH = 'static/tutorials',
93+
CUSTOM_TUTORIALS_PATH = 'static/custom-tutorials',
9194

9295
SLOW_LOGS = 'slow-logs',
9396
SLOW_LOGS_CONFIG = 'slow-logs/config',

redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/EnablementArea.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ describe('EnablementArea', () => {
100100
const { queryByTestId } = render(
101101
<EnablementArea
102102
{...instance(mockedProps)}
103-
guides={{ manual: item }}
103+
tutorials={{ manual: item }}
104104
/>
105105
)
106106
const codeButtonEl = queryByTestId(`preselect-${item.label}`)

redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/EnablementArea.tsx

Lines changed: 150 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,43 @@
1-
import React, { useEffect, useState } from 'react'
1+
import React, { useEffect, useRef, useState } from 'react'
22
import { useHistory, useLocation } from 'react-router-dom'
33
import { useSelector, useDispatch } from 'react-redux'
44
import cx from 'classnames'
55
import { EuiListGroup, EuiLoadingContent } from '@elastic/eui'
6+
import { isEmpty } from 'lodash'
67
import { CodeButtonParams, ExecuteButtonMode } from 'uiSrc/pages/workbench/components/enablement-area/interfaces'
7-
import { EnablementAreaComponent, IEnablementAreaItem } from 'uiSrc/slices/interfaces'
8+
import { DefaultCustomTutorialsItems, EnablementAreaComponent, IEnablementAreaItem } from 'uiSrc/slices/interfaces'
89
import { EnablementAreaProvider, IInternalPage } from 'uiSrc/pages/workbench/contexts/enablementAreaContext'
9-
import { appContextWorkbenchEA, resetWorkbenchEAItem } from 'uiSrc/slices/app/context'
10+
import { appContextWorkbenchEA, resetWorkbenchEASearch } from 'uiSrc/slices/app/context'
1011
import { ApiEndpoints } from 'uiSrc/constants'
11-
import { getWBSourcePath } from './utils/getFileInfo'
12+
import { deleteCustomTutorial, uploadCustomTutorial } from 'uiSrc/slices/workbench/wb-custom-tutorials'
13+
import { Nullable } from 'uiSrc/utils'
14+
import {
15+
getMarkPathDownByManifest,
16+
getWBSourcePath
17+
} from './utils/getFileInfo'
1218
import {
1319
CodeButton,
1420
Group,
1521
InternalLink,
1622
LazyCodeButton,
1723
LazyInternalPage,
1824
PlainText,
25+
UploadTutorialForm,
1926
} from './components'
2027

28+
import {
29+
EAItemActions,
30+
EAManifestFirstKey
31+
} from './constants'
32+
2133
import styles from './styles.module.scss'
2234

2335
const padding = parseInt(styles.paddingHorizontal)
2436

2537
export interface Props {
2638
guides: Record<string, IEnablementAreaItem>
2739
tutorials: Record<string, IEnablementAreaItem>
40+
customTutorials: DefaultCustomTutorialsItems
2841
loading: boolean
2942
openScript: (
3043
script: string,
@@ -39,6 +52,7 @@ const EnablementArea = (props: Props) => {
3952
const {
4053
guides = {},
4154
tutorials = {},
55+
customTutorials = {},
4256
openScript,
4357
loading,
4458
onOpenInternalPage,
@@ -47,65 +61,170 @@ const EnablementArea = (props: Props) => {
4761
const { search } = useLocation()
4862
const history = useHistory()
4963
const dispatch = useDispatch()
50-
const { itemPath: itemFromContext } = useSelector(appContextWorkbenchEA)
64+
const { search: searchEAContext } = useSelector(appContextWorkbenchEA)
5165
const [isInternalPageVisible, setIsInternalPageVisible] = useState(false)
66+
const [isCreateOpen, setIsCreateOpen] = useState(false)
5267
const [internalPage, setInternalPage] = useState<IInternalPage>({ path: '' })
68+
const [manifest, setManifest] = useState<Nullable<Record<string, IEnablementAreaItem>>>(null)
69+
70+
const searchRef = useRef<string>('')
5371

5472
useEffect(() => {
73+
searchRef.current = search
5574
const pagePath = new URLSearchParams(search).get('item')
75+
5676
if (pagePath) {
5777
setIsInternalPageVisible(true)
5878
setInternalPage({ path: pagePath })
5979
return
6080
}
61-
if (itemFromContext) {
62-
handleOpenInternalPage({ path: itemFromContext })
63-
return
81+
82+
const contextPath = new URLSearchParams(searchEAContext).get('item')
83+
84+
if (contextPath) {
85+
handleOpenInternalPage({ path: contextPath })
6486
}
87+
6588
setIsInternalPageVisible(false)
6689
}, [search])
6790

91+
useEffect(() => {
92+
const manifestPath = new URLSearchParams(search).get('path')
93+
const contextManifestPath = new URLSearchParams(searchEAContext).get('path')
94+
const { manifest, prefixFolder } = getManifestByPath(manifestPath)
95+
setManifest(manifest)
96+
97+
if (isEmpty(manifest) && !contextManifestPath) {
98+
return
99+
}
100+
101+
const path = getMarkPathDownByManifest(manifest as Record<string, IEnablementAreaItem>, manifestPath, prefixFolder)
102+
if (path) {
103+
setIsInternalPageVisible(true)
104+
setInternalPage({ path, manifestPath })
105+
106+
return
107+
}
108+
109+
if (contextManifestPath) {
110+
handleOpenInternalPage({ path: '', manifestPath: contextManifestPath })
111+
112+
return
113+
}
114+
115+
setIsInternalPageVisible(false)
116+
}, [search, customTutorials])
117+
118+
const getManifestByPath = (path: Nullable<string> = '') => {
119+
const manifestPath = path?.replace(/^\//, '') || ''
120+
if (manifestPath.startsWith(EAManifestFirstKey.CUSTOM_TUTORIALS)) {
121+
return ({ manifest: customTutorials, prefixFolder: ApiEndpoints.CUSTOM_TUTORIALS_PATH })
122+
}
123+
if (manifestPath.startsWith(EAManifestFirstKey.TUTORIALS)) {
124+
return ({ manifest: tutorials, prefixFolder: ApiEndpoints.TUTORIALS_PATH })
125+
}
126+
if (manifestPath.startsWith(EAManifestFirstKey.GUIDES)) {
127+
return ({ manifest: guides, prefixFolder: ApiEndpoints.GUIDES_PATH })
128+
}
129+
130+
return { manifest: null }
131+
}
132+
133+
const getManifestItems = (manifest: Record<string, IEnablementAreaItem>) =>
134+
Object.keys(manifest).map((key) => ({ ...manifest[key], _key: key }))
135+
68136
const handleOpenInternalPage = (page: IInternalPage) => {
69137
history.push({
70-
search: `?item=${page.path}`
138+
search: page.manifestPath ? `?path=${page.manifestPath}` : `?item=${page.path}`
71139
})
72140
onOpenInternalPage(page)
73141
}
74142

75143
const handleCloseInternalPage = () => {
76-
dispatch(resetWorkbenchEAItem())
144+
dispatch(resetWorkbenchEASearch())
77145
history.push({
78146
// TODO: better to use query-string parser and update only one parameter (instead of replacing all)
79147
search: ''
80148
})
81149
}
82150

83-
const renderSwitch = (item: IEnablementAreaItem, sourcePath: string, level: number) => {
84-
const { label, type, children, id, args } = item
151+
const onDeleteCustomTutorial = (id: string) => {
152+
dispatch(deleteCustomTutorial(id))
153+
}
154+
155+
const submitCreate = ({ file, name }: { file: File, name: string }) => {
156+
const formData = new FormData()
157+
formData.append('file', file)
158+
formData.append('name', name)
159+
160+
dispatch(uploadCustomTutorial(
161+
formData,
162+
() => {
163+
setIsCreateOpen(false)
164+
}
165+
))
166+
}
167+
168+
const renderSwitch = (
169+
item: IEnablementAreaItem,
170+
{ sourcePath, manifestPath = '' }: { sourcePath: string, manifestPath?: string },
171+
level: number,
172+
) => {
173+
const { label, type, children, id, args, _actions: actions, _path: uriPath, _key: key } = item
174+
85175
const paddingsStyle = { paddingLeft: `${padding + level * 8}px`, paddingRight: `${padding}px` }
86-
const borderStyle = { border: 'none', borderTop: '1px solid var(--separatorColor)' }
176+
const currentSourcePath = sourcePath + (uriPath ? `${uriPath}` : (args?.path ?? ''))
177+
const currentManifestPath = (manifestPath + (uriPath ? `${uriPath}` : `/${key}`))
178+
87179
switch (type) {
88180
case EnablementAreaComponent.Group:
89181
return (
90-
<Group triggerStyle={paddingsStyle} testId={id} label={label} {...args}>
91-
{renderTreeView(Object.values(children || {}) || [], sourcePath, level + 1)}
182+
<Group
183+
triggerStyle={paddingsStyle}
184+
id={id}
185+
label={label}
186+
actions={actions}
187+
isShowActions={currentSourcePath.startsWith(ApiEndpoints.CUSTOM_TUTORIALS_PATH)}
188+
onCreate={() => setIsCreateOpen((v) => !v)}
189+
onDelete={onDeleteCustomTutorial}
190+
{...args}
191+
>
192+
<>
193+
{isCreateOpen && actions?.includes(EAItemActions.Create) && (
194+
<UploadTutorialForm onSubmit={submitCreate} onCancel={() => setIsCreateOpen(false)} />
195+
)}
196+
{renderTreeView(
197+
children ? Object.keys(children).map((key) => ({ ...children[key], _key: key })) : [],
198+
{ sourcePath: currentSourcePath, manifestPath: currentManifestPath },
199+
level + 1
200+
)}
201+
</>
92202
</Group>
93203
)
94204
case EnablementAreaComponent.CodeButton:
95205
return (
96206
<>
97-
<div style={paddingsStyle} className="divider"><hr style={borderStyle} /></div>
98-
<div style={{ marginTop: '18px', ...paddingsStyle }}>
207+
<div style={paddingsStyle} className="divider">
208+
<hr style={{ border: 'none', borderTop: '1px solid var(--separatorColor)' }} />
209+
</div>
210+
<div style={{ marginTop: '10px', marginBottom: '10px', ...paddingsStyle }}>
99211
{args?.path
100-
? <LazyCodeButton label={label} {...args} />
212+
? <LazyCodeButton label={label} sourcePath={sourcePath} {...args} />
101213
: <CodeButton onClick={() => openScript(args?.content || '')} label={label} {...args} />}
102214
</div>
103215
</>
104216

105217
)
106218
case EnablementAreaComponent.InternalLink:
107219
return (
108-
<InternalLink sourcePath={sourcePath} style={paddingsStyle} testId={id || label} label={label} {...args}>
220+
<InternalLink
221+
manifestPath={currentManifestPath}
222+
sourcePath={currentSourcePath}
223+
style={paddingsStyle}
224+
testId={id || label}
225+
label={label}
226+
{...args}
227+
>
109228
{args?.content || label}
110229
</InternalLink>
111230
)
@@ -114,10 +233,14 @@ const EnablementArea = (props: Props) => {
114233
}
115234
}
116235

117-
const renderTreeView = (elements: IEnablementAreaItem[], sourcePath: string, level: number = 0) => (
236+
const renderTreeView = (
237+
elements: IEnablementAreaItem[],
238+
paths: { sourcePath: string, manifestPath?: string },
239+
level: number = 0,
240+
) => (
118241
elements?.map((item) => (
119242
<div className="fluid" key={item.id}>
120-
{renderSwitch(item, sourcePath, level)}
243+
{renderSwitch(item, paths, level)}
121244
</div>
122245
)))
123246

@@ -137,8 +260,9 @@ const EnablementArea = (props: Props) => {
137260
flush
138261
className={cx(styles.innerContainer)}
139262
>
140-
{renderTreeView(Object.values(guides), ApiEndpoints.GUIDES_PATH)}
141-
{renderTreeView(Object.values(tutorials), ApiEndpoints.TUTORIALS_PATH)}
263+
{renderTreeView(getManifestItems(guides), { sourcePath: ApiEndpoints.GUIDES_PATH })}
264+
{renderTreeView(getManifestItems(tutorials), { sourcePath: ApiEndpoints.TUTORIALS_PATH })}
265+
{renderTreeView(getManifestItems(customTutorials), { sourcePath: ApiEndpoints.CUSTOM_TUTORIALS_PATH })}
142266
</EuiListGroup>
143267
)}
144268
<div
@@ -152,7 +276,10 @@ const EnablementArea = (props: Props) => {
152276
onClose={handleCloseInternalPage}
153277
title={internalPage?.label}
154278
path={internalPage?.path}
279+
manifest={manifest}
280+
manifestPath={internalPage?.manifestPath}
155281
sourcePath={getWBSourcePath(internalPage?.path)}
282+
search={searchRef.current}
156283
/>
157284
)}
158285
</div>

redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/Code/Code.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const Code = ({ children, params = '', ...rest }: Props) => {
2929
let file: Maybe<{ path: string, name: string }>
3030

3131
if (pagePath) {
32-
const pageInfo = getFileInfo(pagePath)
32+
const pageInfo = getFileInfo({ path: pagePath })
3333
file = {
3434
path: `${pageInfo.location}/${pageInfo.name}`,
3535
name: startCase(rest.label)

0 commit comments

Comments
 (0)