Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions redisinsight/ui/src/components/uploadFile/UploadFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { EuiButtonEmpty, EuiText } from '@elastic/eui'
import styles from './styles.module.scss'

export interface Props {
onFileChange: ({ target: { files } }: { target: { files: FileList | null } }) => void
onClick: () => void
onFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void
onClick?: () => void
accept?: string
}

const UploadFile = ({ onFileChange, onClick }: Props) => (
const UploadFile = ({ onFileChange, onClick, accept }: Props) => (
<EuiButtonEmpty
iconType="folderOpen"
className={styles.emptyBtn}
Expand All @@ -19,9 +20,9 @@ const UploadFile = ({ onFileChange, onClick }: Props) => (
type="file"
id="upload-input-file"
data-testid="upload-input-file"
accept="application/json, text/plain"
accept={accept || '*'}
onChange={onFileChange}
onClick={onClick}
onClick={() => onClick?.()}
className={styles.fileDrop}
aria-label="Select file"
/>
Expand Down
3 changes: 3 additions & 0 deletions redisinsight/ui/src/constants/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,14 @@ enum ApiEndpoints {
GUIDES = 'static/guides/guides.json',
// TODO double check it, when tutorials will be completed
TUTORIALS = 'static/tutorials/tutorials.json',
CUSTOM_TUTORIALS = 'custom-tutorials',
CUSTOM_TUTORIALS_MANIFEST = 'custom-tutorials/manifest',
PLUGINS = 'plugins',
STATE = 'state',
CONTENT_CREATE_DATABASE = 'static/content/create-redis.json',
GUIDES_PATH = 'static/guides',
TUTORIALS_PATH = 'static/tutorials',
CUSTOM_TUTORIALS_PATH = 'static/custom-tutorials',

SLOW_LOGS = 'slow-logs',
SLOW_LOGS_CONFIG = 'slow-logs/config',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ describe('EnablementArea', () => {
const { queryByTestId } = render(
<EnablementArea
{...instance(mockedProps)}
guides={{ manual: item }}
tutorials={{ manual: item }}
/>
)
const codeButtonEl = queryByTestId(`preselect-${item.label}`)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import cx from 'classnames'
import { EuiListGroup, EuiLoadingContent } from '@elastic/eui'
import { isEmpty } from 'lodash'
import { CodeButtonParams, ExecuteButtonMode } from 'uiSrc/pages/workbench/components/enablement-area/interfaces'
import { EnablementAreaComponent, IEnablementAreaItem } from 'uiSrc/slices/interfaces'
import { DefaultCustomTutorialsItems, EnablementAreaComponent, IEnablementAreaItem } from 'uiSrc/slices/interfaces'
import { EnablementAreaProvider, IInternalPage } from 'uiSrc/pages/workbench/contexts/enablementAreaContext'
import { appContextWorkbenchEA, resetWorkbenchEAItem } from 'uiSrc/slices/app/context'
import { appContextWorkbenchEA, resetWorkbenchEASearch } from 'uiSrc/slices/app/context'
import { ApiEndpoints } from 'uiSrc/constants'
import { getWBSourcePath } from './utils/getFileInfo'
import { deleteCustomTutorial, uploadCustomTutorial } from 'uiSrc/slices/workbench/wb-custom-tutorials'
import { Nullable } from 'uiSrc/utils'
import {
getMarkPathDownByManifest,
getWBSourcePath
} from './utils/getFileInfo'
import {
CodeButton,
Group,
InternalLink,
LazyCodeButton,
LazyInternalPage,
PlainText,
UploadTutorialForm,
} from './components'

import {
EAItemActions,
EAManifestFirstKey
} from './constants'

import styles from './styles.module.scss'

const padding = parseInt(styles.paddingHorizontal)

export interface Props {
guides: Record<string, IEnablementAreaItem>
tutorials: Record<string, IEnablementAreaItem>
customTutorials: DefaultCustomTutorialsItems
loading: boolean
openScript: (
script: string,
Expand All @@ -39,6 +52,7 @@ const EnablementArea = (props: Props) => {
const {
guides = {},
tutorials = {},
customTutorials = {},
openScript,
loading,
onOpenInternalPage,
Expand All @@ -47,65 +61,170 @@ const EnablementArea = (props: Props) => {
const { search } = useLocation()
const history = useHistory()
const dispatch = useDispatch()
const { itemPath: itemFromContext } = useSelector(appContextWorkbenchEA)
const { search: searchEAContext } = useSelector(appContextWorkbenchEA)
const [isInternalPageVisible, setIsInternalPageVisible] = useState(false)
const [isCreateOpen, setIsCreateOpen] = useState(false)
const [internalPage, setInternalPage] = useState<IInternalPage>({ path: '' })
const [manifest, setManifest] = useState<Nullable<Record<string, IEnablementAreaItem>>>(null)

const searchRef = useRef<string>('')

useEffect(() => {
searchRef.current = search
const pagePath = new URLSearchParams(search).get('item')

if (pagePath) {
setIsInternalPageVisible(true)
setInternalPage({ path: pagePath })
return
}
if (itemFromContext) {
handleOpenInternalPage({ path: itemFromContext })
return

const contextPath = new URLSearchParams(searchEAContext).get('item')

if (contextPath) {
handleOpenInternalPage({ path: contextPath })
}

setIsInternalPageVisible(false)
}, [search])

useEffect(() => {
const manifestPath = new URLSearchParams(search).get('path')
const contextManifestPath = new URLSearchParams(searchEAContext).get('path')
const { manifest, prefixFolder } = getManifestByPath(manifestPath)
setManifest(manifest)

if (isEmpty(manifest) && !contextManifestPath) {
return
}

const path = getMarkPathDownByManifest(manifest as Record<string, IEnablementAreaItem>, manifestPath, prefixFolder)
if (path) {
setIsInternalPageVisible(true)
setInternalPage({ path, manifestPath })

return
}

if (contextManifestPath) {
handleOpenInternalPage({ path: '', manifestPath: contextManifestPath })

return
}

setIsInternalPageVisible(false)
}, [search, customTutorials])

const getManifestByPath = (path: Nullable<string> = '') => {
const manifestPath = path?.replace(/^\//, '') || ''
if (manifestPath.startsWith(EAManifestFirstKey.CUSTOM_TUTORIALS)) {
return ({ manifest: customTutorials, prefixFolder: ApiEndpoints.CUSTOM_TUTORIALS_PATH })
}
if (manifestPath.startsWith(EAManifestFirstKey.TUTORIALS)) {
return ({ manifest: tutorials, prefixFolder: ApiEndpoints.TUTORIALS_PATH })
}
if (manifestPath.startsWith(EAManifestFirstKey.GUIDES)) {
return ({ manifest: guides, prefixFolder: ApiEndpoints.GUIDES_PATH })
}

return { manifest: null }
}

const getManifestItems = (manifest: Record<string, IEnablementAreaItem>) =>
Object.keys(manifest).map((key) => ({ ...manifest[key], _key: key }))

const handleOpenInternalPage = (page: IInternalPage) => {
history.push({
search: `?item=${page.path}`
search: page.manifestPath ? `?path=${page.manifestPath}` : `?item=${page.path}`
})
onOpenInternalPage(page)
}

const handleCloseInternalPage = () => {
dispatch(resetWorkbenchEAItem())
dispatch(resetWorkbenchEASearch())
history.push({
// TODO: better to use query-string parser and update only one parameter (instead of replacing all)
search: ''
})
}

const renderSwitch = (item: IEnablementAreaItem, sourcePath: string, level: number) => {
const { label, type, children, id, args } = item
const onDeleteCustomTutorial = (id: string) => {
dispatch(deleteCustomTutorial(id))
}

const submitCreate = ({ file, name }: { file: File, name: string }) => {
const formData = new FormData()
formData.append('file', file)
formData.append('name', name)

dispatch(uploadCustomTutorial(
formData,
() => {
setIsCreateOpen(false)
}
))
}

const renderSwitch = (
item: IEnablementAreaItem,
{ sourcePath, manifestPath = '' }: { sourcePath: string, manifestPath?: string },
level: number,
) => {
const { label, type, children, id, args, _actions: actions, _path: uriPath, _key: key } = item

const paddingsStyle = { paddingLeft: `${padding + level * 8}px`, paddingRight: `${padding}px` }
const borderStyle = { border: 'none', borderTop: '1px solid var(--separatorColor)' }
const currentSourcePath = sourcePath + (uriPath ? `${uriPath}` : (args?.path ?? ''))
const currentManifestPath = (manifestPath + (uriPath ? `${uriPath}` : `/${key}`))

switch (type) {
case EnablementAreaComponent.Group:
return (
<Group triggerStyle={paddingsStyle} testId={id} label={label} {...args}>
{renderTreeView(Object.values(children || {}) || [], sourcePath, level + 1)}
<Group
triggerStyle={paddingsStyle}
id={id}
label={label}
actions={actions}
isShowActions={currentSourcePath.startsWith(ApiEndpoints.CUSTOM_TUTORIALS_PATH)}
onCreate={() => setIsCreateOpen((v) => !v)}
onDelete={onDeleteCustomTutorial}
{...args}
>
<>
{isCreateOpen && actions?.includes(EAItemActions.Create) && (
<UploadTutorialForm onSubmit={submitCreate} onCancel={() => setIsCreateOpen(false)} />
)}
{renderTreeView(
children ? Object.keys(children).map((key) => ({ ...children[key], _key: key })) : [],
{ sourcePath: currentSourcePath, manifestPath: currentManifestPath },
level + 1
)}
</>
</Group>
)
case EnablementAreaComponent.CodeButton:
return (
<>
<div style={paddingsStyle} className="divider"><hr style={borderStyle} /></div>
<div style={{ marginTop: '18px', ...paddingsStyle }}>
<div style={paddingsStyle} className="divider">
<hr style={{ border: 'none', borderTop: '1px solid var(--separatorColor)' }} />
</div>
<div style={{ marginTop: '10px', marginBottom: '10px', ...paddingsStyle }}>
{args?.path
? <LazyCodeButton label={label} {...args} />
? <LazyCodeButton label={label} sourcePath={sourcePath} {...args} />
: <CodeButton onClick={() => openScript(args?.content || '')} label={label} {...args} />}
</div>
</>

)
case EnablementAreaComponent.InternalLink:
return (
<InternalLink sourcePath={sourcePath} style={paddingsStyle} testId={id || label} label={label} {...args}>
<InternalLink
manifestPath={currentManifestPath}
sourcePath={currentSourcePath}
style={paddingsStyle}
testId={id || label}
label={label}
{...args}
>
{args?.content || label}
</InternalLink>
)
Expand All @@ -114,10 +233,14 @@ const EnablementArea = (props: Props) => {
}
}

const renderTreeView = (elements: IEnablementAreaItem[], sourcePath: string, level: number = 0) => (
const renderTreeView = (
elements: IEnablementAreaItem[],
paths: { sourcePath: string, manifestPath?: string },
level: number = 0,
) => (
elements?.map((item) => (
<div className="fluid" key={item.id}>
{renderSwitch(item, sourcePath, level)}
{renderSwitch(item, paths, level)}
</div>
)))

Expand All @@ -137,8 +260,9 @@ const EnablementArea = (props: Props) => {
flush
className={cx(styles.innerContainer)}
>
{renderTreeView(Object.values(guides), ApiEndpoints.GUIDES_PATH)}
{renderTreeView(Object.values(tutorials), ApiEndpoints.TUTORIALS_PATH)}
{renderTreeView(getManifestItems(guides), { sourcePath: ApiEndpoints.GUIDES_PATH })}
{renderTreeView(getManifestItems(tutorials), { sourcePath: ApiEndpoints.TUTORIALS_PATH })}
{renderTreeView(getManifestItems(customTutorials), { sourcePath: ApiEndpoints.CUSTOM_TUTORIALS_PATH })}
</EuiListGroup>
)}
<div
Expand All @@ -152,7 +276,10 @@ const EnablementArea = (props: Props) => {
onClose={handleCloseInternalPage}
title={internalPage?.label}
path={internalPage?.path}
manifest={manifest}
manifestPath={internalPage?.manifestPath}
sourcePath={getWBSourcePath(internalPage?.path)}
search={searchRef.current}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const Code = ({ children, params = '', ...rest }: Props) => {
let file: Maybe<{ path: string, name: string }>

if (pagePath) {
const pageInfo = getFileInfo(pagePath)
const pageInfo = getFileInfo({ path: pagePath })
file = {
path: `${pageInfo.location}/${pageInfo.name}`,
name: startCase(rest.label)
Expand Down
Loading