feat: flows index (#19448)
* feat(flows): empty state for index page * feat(wip): link to flows from index page * feat(wip): rename flows * feat: name flows * fix: set feature flags * test: skip flagged testpull/19455/head
parent
684b1149f3
commit
6151c389f0
|
@ -0,0 +1,26 @@
|
|||
describe('Flows', () => {
|
||||
beforeEach(() => {
|
||||
cy.flush()
|
||||
cy.signin().then(({body}) => {
|
||||
const {
|
||||
org: {id},
|
||||
bucket,
|
||||
} = body
|
||||
cy.wrap(body.org).as('org')
|
||||
cy.wrap(bucket).as('bucket')
|
||||
cy.fixture('routes').then(({orgs, flows}) => {
|
||||
cy.visit(`${orgs}/${id}${flows}`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: unskip when no longer blocked by feature flag
|
||||
it.skip('CRUD a flow from the index page', () => {
|
||||
cy.getByTestID('create-flow--button')
|
||||
.first()
|
||||
.click()
|
||||
|
||||
cy.getByTestID('page-title').click()
|
||||
cy.getByTestID('renamable-page-title--input').type('My Flow {enter}')
|
||||
})
|
||||
})
|
|
@ -7,5 +7,6 @@
|
|||
"endpoints": "/endpoints",
|
||||
"rules": "/rules",
|
||||
"buckets": "/load-data/buckets",
|
||||
"telegrafs": "/load-data/telegrafs"
|
||||
"telegrafs": "/load-data/telegrafs",
|
||||
"flows": "/flows"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import React, {FC} from 'react'
|
||||
import {useParams, useHistory} from 'react-router-dom'
|
||||
|
||||
// Components
|
||||
import {ResourceCard} from '@influxdata/clockface'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const FlowCard: FC<Props> = ({id, name}) => {
|
||||
const {orgID} = useParams()
|
||||
const history = useHistory()
|
||||
|
||||
const handleClick = () => {
|
||||
history.push(`/orgs/${orgID}/notebooks/${id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceCard key={`flow-card--${id}`}>
|
||||
<ResourceCard.Name
|
||||
name={name || 'Name this flow'}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</ResourceCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default FlowCard
|
|
@ -0,0 +1,32 @@
|
|||
import React, {useContext} from 'react'
|
||||
|
||||
import {ResourceList, Grid, Columns} from '@influxdata/clockface'
|
||||
import {NotebookListContext} from 'src/notebooks/context/notebook.list'
|
||||
import FlowsIndexEmpty from 'src/notebooks/components/FlowsIndexEmpty'
|
||||
|
||||
import FlowCard from 'src/notebooks/components/FlowCard'
|
||||
|
||||
const FlowCards = () => {
|
||||
const {notebooks} = useContext(NotebookListContext)
|
||||
return (
|
||||
<Grid>
|
||||
<Grid.Row>
|
||||
<Grid.Column
|
||||
widthXS={Columns.Twelve}
|
||||
widthSM={Columns.Eight}
|
||||
widthMD={Columns.Ten}
|
||||
>
|
||||
<ResourceList>
|
||||
<ResourceList.Body emptyState={<FlowsIndexEmpty />}>
|
||||
{Object.entries(notebooks).map(([id, {name}]) => {
|
||||
return <FlowCard key={id} id={id} name={name} />
|
||||
})}
|
||||
</ResourceList.Body>
|
||||
</ResourceList>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default FlowCards
|
|
@ -0,0 +1,31 @@
|
|||
// Libraries
|
||||
import React, {useContext} from 'react'
|
||||
import {useHistory, useParams} from 'react-router-dom'
|
||||
|
||||
// Components
|
||||
import {Button, IconFont, ComponentColor} from '@influxdata/clockface'
|
||||
import {NotebookListContext} from 'src/notebooks/context/notebook.list'
|
||||
|
||||
const FlowCreateButton = () => {
|
||||
const history = useHistory()
|
||||
const {orgID} = useParams()
|
||||
const {add} = useContext(NotebookListContext)
|
||||
|
||||
const handleCreate = async () => {
|
||||
const id = await add()
|
||||
history.push(`/orgs/${orgID}/notebooks/${id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={IconFont.Plus}
|
||||
color={ComponentColor.Primary}
|
||||
text="Create Flow"
|
||||
titleText="Click to create a Flow"
|
||||
onClick={handleCreate}
|
||||
testID="create-flow--button"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default FlowCreateButton
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react'
|
||||
|
||||
// Components
|
||||
import {Page} from '@influxdata/clockface'
|
||||
import FlowHeader from 'src/notebooks/components/header'
|
||||
import PipeList from 'src/notebooks/components/PipeList'
|
||||
import MiniMap from 'src/notebooks/components/minimap/MiniMap'
|
||||
|
||||
const FlowPage = () => {
|
||||
return (
|
||||
<Page titleTag="Flows">
|
||||
<FlowHeader />
|
||||
<Page.Contents
|
||||
fullWidth={true}
|
||||
scrollable={false}
|
||||
className="notebook-page"
|
||||
>
|
||||
<div className="notebook">
|
||||
<MiniMap />
|
||||
<PipeList />
|
||||
</div>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default FlowPage
|
|
@ -0,0 +1,32 @@
|
|||
// Libraries
|
||||
import React from 'react'
|
||||
|
||||
// Components
|
||||
import {Page, PageHeader} from '@influxdata/clockface'
|
||||
import FlowCreateButton from 'src/notebooks/components/FlowCreateButton'
|
||||
import NotebookListProvider from 'src/notebooks/context/notebook.list'
|
||||
import FlowCards from 'src/notebooks/components/FlowCards'
|
||||
|
||||
// Utils
|
||||
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
|
||||
|
||||
const FlowsIndex = () => {
|
||||
return (
|
||||
<NotebookListProvider>
|
||||
<Page titleTag={pageTitleSuffixer(['Flows'])} testID="flows-index">
|
||||
<PageHeader fullWidth={false}>
|
||||
<Page.Title title="Flows" />
|
||||
</PageHeader>
|
||||
<Page.ControlBar fullWidth={false}>
|
||||
<Page.ControlBarLeft></Page.ControlBarLeft>
|
||||
<Page.ControlBarRight>
|
||||
<FlowCreateButton />
|
||||
</Page.ControlBarRight>
|
||||
</Page.ControlBar>
|
||||
<FlowCards />
|
||||
</Page>
|
||||
</NotebookListProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default FlowsIndex
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react'
|
||||
import {ComponentSize, EmptyState} from '@influxdata/clockface'
|
||||
import FlowCreateButton from './FlowCreateButton'
|
||||
|
||||
const FlowsIndexEmpty = () => {
|
||||
return (
|
||||
<EmptyState size={ComponentSize.Large}>
|
||||
<EmptyState.Text>
|
||||
Looks like there aren't any <b>Flows</b>, why not create one?
|
||||
</EmptyState.Text>
|
||||
<FlowCreateButton />
|
||||
</EmptyState>
|
||||
)
|
||||
}
|
||||
|
||||
export default FlowsIndexEmpty
|
|
@ -1,18 +1,26 @@
|
|||
// Libraries
|
||||
import React, {FC} from 'react'
|
||||
import React, {FC, useContext, useEffect} from 'react'
|
||||
import {useParams} from 'react-router-dom'
|
||||
|
||||
// Components
|
||||
import {Page} from '@influxdata/clockface'
|
||||
import NotebookHeader from 'src/notebooks/components/header'
|
||||
import PipeList from 'src/notebooks/components/PipeList'
|
||||
import MiniMap from 'src/notebooks/components/minimap/MiniMap'
|
||||
|
||||
// Contexts
|
||||
import {ResultsProvider} from 'src/notebooks/context/results'
|
||||
import {RefProvider} from 'src/notebooks/context/refs'
|
||||
import CurrentNotebook from 'src/notebooks/context/notebook.current'
|
||||
import CurrentNotebookProvider, {
|
||||
NotebookContext,
|
||||
} from 'src/notebooks/context/notebook.current'
|
||||
import {ScrollProvider} from 'src/notebooks/context/scroll'
|
||||
import FlowPage from 'src/notebooks/components/FlowPage'
|
||||
|
||||
const NotebookFromRoute = () => {
|
||||
const {id} = useParams()
|
||||
const {change} = useContext(NotebookContext)
|
||||
|
||||
useEffect(() => {
|
||||
change(id)
|
||||
}, [id, change])
|
||||
|
||||
return null
|
||||
}
|
||||
// NOTE: uncommon, but using this to scope the project
|
||||
// within the page and not bleed it's dependancies outside
|
||||
// of the feature flag
|
||||
|
@ -20,27 +28,16 @@ import 'src/notebooks/style.scss'
|
|||
|
||||
const NotebookPage: FC = () => {
|
||||
return (
|
||||
<CurrentNotebook>
|
||||
<CurrentNotebookProvider>
|
||||
<NotebookFromRoute />
|
||||
<ResultsProvider>
|
||||
<RefProvider>
|
||||
<ScrollProvider>
|
||||
<Page titleTag="Flows">
|
||||
<NotebookHeader />
|
||||
<Page.Contents
|
||||
fullWidth={true}
|
||||
scrollable={false}
|
||||
className="notebook-page"
|
||||
>
|
||||
<div className="notebook">
|
||||
<MiniMap />
|
||||
<PipeList />
|
||||
</div>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
<FlowPage />
|
||||
</ScrollProvider>
|
||||
</RefProvider>
|
||||
</ResultsProvider>
|
||||
</CurrentNotebook>
|
||||
</CurrentNotebookProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import React, {FC, useContext, useCallback} from 'react'
|
||||
|
||||
// Contexts
|
||||
import {NotebookContext} from 'src/notebooks/context/notebook'
|
||||
import {NotebookContext} from 'src/notebooks/context/notebook.current'
|
||||
import {TimeProvider, TimeContext, TimeBlock} from 'src/notebooks/context/time'
|
||||
import AppSettingProvider from 'src/notebooks/context/app'
|
||||
|
||||
|
@ -13,6 +13,7 @@ import TimeRangeDropdown from 'src/notebooks/components/header/TimeRangeDropdown
|
|||
import AutoRefreshDropdown from 'src/notebooks/components/header/AutoRefreshDropdown'
|
||||
import Submit from 'src/notebooks/components/header/Submit'
|
||||
import PresentationMode from 'src/notebooks/components/header/PresentationMode'
|
||||
import RenamablePageTitle from 'src/pageLayout/components/RenamablePageTitle'
|
||||
|
||||
const FULL_WIDTH = true
|
||||
|
||||
|
@ -22,12 +23,12 @@ export interface TimeContextProps {
|
|||
}
|
||||
|
||||
const NotebookHeader: FC = () => {
|
||||
const {id} = useContext(NotebookContext)
|
||||
const {id, update, notebook} = useContext(NotebookContext)
|
||||
const {timeContext, addTimeContext, updateTimeContext} = useContext(
|
||||
TimeContext
|
||||
)
|
||||
|
||||
const update = useCallback(
|
||||
const updateTime = useCallback(
|
||||
(data: TimeBlock) => {
|
||||
updateTimeContext(id, data)
|
||||
},
|
||||
|
@ -39,10 +40,19 @@ const NotebookHeader: FC = () => {
|
|||
return null
|
||||
}
|
||||
|
||||
const handleRename = (name: string) => {
|
||||
update({...notebook, name})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Page.Header fullWidth={FULL_WIDTH}>
|
||||
<Page.Title title="Flows" />
|
||||
<RenamablePageTitle
|
||||
onRename={handleRename}
|
||||
name={notebook.name}
|
||||
placeholder="Name this Flow"
|
||||
maxLength={50}
|
||||
/>
|
||||
</Page.Header>
|
||||
<Page.ControlBar fullWidth={FULL_WIDTH}>
|
||||
<Page.ControlBarLeft>
|
||||
|
@ -51,8 +61,8 @@ const NotebookHeader: FC = () => {
|
|||
<Page.ControlBarRight>
|
||||
<PresentationMode />
|
||||
<TimeZoneDropdown />
|
||||
<TimeRangeDropdown context={timeContext[id]} update={update} />
|
||||
<AutoRefreshDropdown context={timeContext[id]} update={update} />
|
||||
<TimeRangeDropdown context={timeContext[id]} update={updateTime} />
|
||||
<AutoRefreshDropdown context={timeContext[id]} update={updateTime} />
|
||||
</Page.ControlBarRight>
|
||||
</Page.ControlBar>
|
||||
</>
|
||||
|
|
|
@ -12,6 +12,7 @@ const useNotebookCurrentState = createPersistedState('current-notebook')
|
|||
|
||||
export interface NotebookContextType {
|
||||
id: string | null
|
||||
name: string
|
||||
notebook: Notebook | null
|
||||
change: (id: string) => void
|
||||
add: (data: Partial<PipeData>, index?: number) => string
|
||||
|
@ -21,6 +22,7 @@ export interface NotebookContextType {
|
|||
|
||||
export const DEFAULT_CONTEXT: NotebookContextType = {
|
||||
id: null,
|
||||
name: 'Name this Flow',
|
||||
notebook: null,
|
||||
add: () => '',
|
||||
change: () => {},
|
||||
|
@ -114,6 +116,7 @@ export const NotebookProvider: FC = ({children}) => {
|
|||
<NotebookContext.Provider
|
||||
value={{
|
||||
id: currentID,
|
||||
name,
|
||||
notebook: notebooks[currentID],
|
||||
add: addPipe,
|
||||
update: updateCurrent,
|
||||
|
|
|
@ -20,6 +20,7 @@ export interface NotebookListContextType extends NotebookList {
|
|||
}
|
||||
|
||||
export const EMPTY_NOTEBOOK: NotebookState = {
|
||||
name: 'Name this Flow',
|
||||
data: {
|
||||
byID: {},
|
||||
allIDs: [],
|
||||
|
@ -57,6 +58,7 @@ export const NotebookListProvider: FC = ({children}) => {
|
|||
}
|
||||
} else {
|
||||
_notebook = {
|
||||
name: notebook.name,
|
||||
data: notebook.data.serialize(),
|
||||
meta: notebook.meta.serialize(),
|
||||
readOnly: notebook.readOnly,
|
||||
|
@ -79,6 +81,7 @@ export const NotebookListProvider: FC = ({children}) => {
|
|||
setNotebooks({
|
||||
...notebooks,
|
||||
[id]: {
|
||||
name: notebook.name,
|
||||
data: notebook.data.serialize(),
|
||||
meta: notebook.meta.serialize(),
|
||||
readOnly: notebook.readOnly,
|
||||
|
@ -111,6 +114,7 @@ export const NotebookListProvider: FC = ({children}) => {
|
|||
}
|
||||
|
||||
acc[curr] = {
|
||||
name: notebooks[curr].name,
|
||||
data: _asResource(notebooks[curr].data, data => {
|
||||
stateUpdater('data', data)
|
||||
}),
|
||||
|
|
|
@ -14,6 +14,7 @@ export interface PipeMeta {
|
|||
// TODO: this is screaming for normalization. figure out frontend uuids for cells
|
||||
export interface NotebookContextType {
|
||||
id: string
|
||||
name: string
|
||||
pipes: PipeData[]
|
||||
meta: PipeMeta[] // data only used for the view layer for Notebooks
|
||||
results: FluxResult[]
|
||||
|
@ -21,16 +22,19 @@ export interface NotebookContextType {
|
|||
updatePipe: (idx: number, pipe: Partial<PipeData>) => void
|
||||
updateMeta: (idx: number, pipe: Partial<PipeMeta>) => void
|
||||
updateResult: (idx: number, result: Partial<FluxResult>) => void
|
||||
updateName: (name: string) => void
|
||||
movePipe: (currentIdx: number, newIdx: number) => void
|
||||
removePipe: (idx: number) => void
|
||||
}
|
||||
|
||||
export const DEFAULT_CONTEXT: NotebookContextType = {
|
||||
id: 'new',
|
||||
name: 'Name this Flow',
|
||||
pipes: [],
|
||||
meta: [],
|
||||
results: [],
|
||||
addPipe: () => {},
|
||||
updateName: () => {},
|
||||
updatePipe: () => {},
|
||||
updateMeta: () => {},
|
||||
updateResult: () => {},
|
||||
|
@ -78,11 +82,14 @@ export const NotebookProvider: FC = ({children}) => {
|
|||
const [pipes, setPipes] = useState(DEFAULT_CONTEXT.pipes)
|
||||
const [meta, setMeta] = useState(DEFAULT_CONTEXT.meta)
|
||||
const [results, setResults] = useState(DEFAULT_CONTEXT.results)
|
||||
const [name, setName] = useState(DEFAULT_CONTEXT.name)
|
||||
|
||||
const _setPipes = useCallback(setPipes, [id, setPipes])
|
||||
const _setMeta = useCallback(setMeta, [id, setMeta])
|
||||
const _setResults = useCallback(setResults, [id, setResults])
|
||||
|
||||
const updateName = (newName: string) => setName(newName)
|
||||
|
||||
const addPipe = useCallback(
|
||||
(pipe: PipeData, insertAtIndex?: number) => {
|
||||
let add = data => {
|
||||
|
@ -221,9 +228,11 @@ export const NotebookProvider: FC = ({children}) => {
|
|||
<NotebookContext.Provider
|
||||
value={{
|
||||
id,
|
||||
name,
|
||||
pipes,
|
||||
meta,
|
||||
results,
|
||||
updateName,
|
||||
updatePipe,
|
||||
updateMeta,
|
||||
updateResult,
|
||||
|
|
|
@ -56,12 +56,14 @@ export interface ResourceManipulator<T> {
|
|||
}
|
||||
|
||||
export interface NotebookState {
|
||||
name: string
|
||||
data: Resource<PipeData>
|
||||
meta: Resource<PipeMeta>
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
export interface Notebook {
|
||||
name: string
|
||||
data: ResourceManipulator<PipeData>
|
||||
meta: ResourceManipulator<PipeMeta>
|
||||
results: FluxResult
|
||||
|
|
|
@ -108,17 +108,17 @@ export const generateNavItems = (orgID: string): NavItem[] => {
|
|||
activeKeywords: ['data-explorer'],
|
||||
},
|
||||
{
|
||||
id: 'notebooks',
|
||||
testID: 'nav-item-notebooks',
|
||||
id: 'flows',
|
||||
testID: 'nav-item-flows',
|
||||
icon: IconFont.Erlenmeyer,
|
||||
label: 'Flows',
|
||||
featureFlag: 'notebooks',
|
||||
shortLabel: 'Flows',
|
||||
link: {
|
||||
type: 'link',
|
||||
location: `${orgPrefix}/notebooks`,
|
||||
location: `${orgPrefix}/flows`,
|
||||
},
|
||||
activeKeywords: ['notebooks'],
|
||||
activeKeywords: ['flows'],
|
||||
},
|
||||
{
|
||||
id: 'dashboards',
|
||||
|
|
|
@ -29,6 +29,7 @@ import MembersIndex from 'src/members/containers/MembersIndex'
|
|||
import RouteToDashboardList from 'src/dashboards/components/RouteToDashboardList'
|
||||
import ClientLibrariesPage from 'src/writeData/containers/ClientLibrariesPage'
|
||||
import TelegrafPluginsPage from 'src/writeData/containers/TelegrafPluginsPage'
|
||||
import FlowsIndex from 'src/notebooks/components/FlowsIndex'
|
||||
|
||||
// Types
|
||||
import {AppState, Organization, ResourceType} from 'src/types'
|
||||
|
@ -134,9 +135,13 @@ const SetOrg: FC<Props> = ({
|
|||
component={RouteToDashboardList}
|
||||
/>
|
||||
|
||||
{/* Flows */}
|
||||
{/* Flows */}
|
||||
{isFlagEnabled('notebooks') && (
|
||||
<Route path={`${orgPath}/notebooks`} component={NotebookPage} />
|
||||
<Route path={`${orgPath}/notebooks/:id`} component={NotebookPage} />
|
||||
)}
|
||||
|
||||
{isFlagEnabled('notebooks') && (
|
||||
<Route path={`${orgPath}/flows`} component={FlowsIndex} />
|
||||
)}
|
||||
|
||||
{/* Write Data */}
|
||||
|
|
Loading…
Reference in New Issue