refactor(templates): Remove legacy templates (#19300)

pull/19435/head
Deniz Kusefoglu 2020-08-25 08:47:01 -07:00 committed by GitHub
parent a03745e57d
commit 543bbd12c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 128 additions and 5497 deletions

View File

@ -28,7 +28,7 @@ describe('Dashboards', () => {
"Looks like you don't have any Dashboards, why not create one?"
)
})
cy.getByTestID('add-resource-dropdown--button').should($b => {
cy.getByTestID('add-resource-button').should($b => {
expect($b).to.have.length(1)
expect($b).to.contain('Create Dashboard')
})
@ -39,18 +39,16 @@ describe('Dashboards', () => {
it('can CRUD dashboards from empty state, header, and a Template', () => {
// Create from empty state
cy.getByTestID('empty-dashboards-list').within(() => {
cy.getByTestID('add-resource-dropdown--button').click()
})
cy.getByTestID('add-resource-dropdown--new')
.click()
.then(() => {
cy.fixture('routes').then(({orgs}) => {
cy.get('@org').then(({id}: Organization) => {
cy.visit(`${orgs}/${id}/dashboards-list`)
cy.getByTestID('add-resource-button')
.click()
.then(() => {
cy.fixture('routes').then(({orgs}) => {
cy.get('@org').then(({id}: Organization) => {
cy.visit(`${orgs}/${id}/dashboards-list`)
})
})
})
})
})
const newName = 'new 🅱ashboard'
@ -70,14 +68,8 @@ describe('Dashboards', () => {
cy.getByTestID('dashboard-card').should('contain', newName)
// Open Export overlay
cy.getByTestID('context-menu-item-export').click({force: true})
cy.getByTestID('export-overlay--text-area').should('exist')
cy.get('.cf-overlay--dismiss').click()
// Create from header
cy.getByTestID('add-resource-dropdown--button').click()
cy.getByTestID('add-resource-dropdown--new').click()
cy.getByTestID('add-resource-button').click()
cy.fixture('routes').then(({orgs}) => {
cy.get('@org').then(({id}: Organization) => {
@ -85,21 +77,6 @@ describe('Dashboards', () => {
})
})
// Create from Template
cy.get('@org').then(({id}: Organization) => {
cy.createDashboardTemplate(id)
})
cy.getByTestID('empty-dashboards-list').within(() => {
cy.getByTestID('add-resource-dropdown--button').click()
cy.getByTestID('add-resource-dropdown--template').click()
})
cy.getByTestID('template--Bashboard-Template').click()
cy.getByTestID('template-panel').should('exist')
cy.getByTestID('create-dashboard-button').click()
cy.getByTestID('dashboard-card').should('have.length', 3)
// Delete dashboards
cy.getByTestID('dashboard-card')
.first()
@ -117,42 +94,9 @@ describe('Dashboards', () => {
cy.getByTestID('context-delete-dashboard').click()
})
cy.getByTestID('dashboard-card')
.first()
.trigger('mouseover')
.within(() => {
cy.getByTestID('context-delete-menu').click()
cy.getByTestID('context-delete-dashboard').click()
})
cy.getByTestID('empty-dashboards-list').should('exist')
})
it('keeps user input in text area when attempting to import invalid JSON', () => {
cy.getByTestID('page-control-bar').within(() => {
cy.getByTestID('add-resource-dropdown--button').click()
})
cy.getByTestID('add-resource-dropdown--import').click()
cy.contains('Paste').click()
cy.getByTestID('import-overlay--textarea')
.click()
.type('this is invalid JSON')
cy.get('button[title*="Import JSON"]').click()
cy.getByTestID('import-overlay--textarea--error').should('have.length', 1)
cy.getByTestID('import-overlay--textarea').should($s =>
expect($s).to.contain('this is invalid JSON')
)
cy.getByTestID('import-overlay--textarea').type(
'{backspace}{backspace}{backspace}{backspace}{backspace}'
)
cy.get('button[title*="Import JSON"]').click()
cy.getByTestID('import-overlay--textarea--error').should('have.length', 1)
cy.getByTestID('import-overlay--textarea').should($s =>
expect($s).to.contain('this is invalid')
)
})
describe('Dashboard List', () => {
beforeEach(() => {
cy.get('@org').then(({id}: Organization) => {

View File

@ -60,19 +60,7 @@ from(bucket: "${name}"{rightarrow}
.should('have.length', 1)
.and('contain', taskName)
// TODO: extend to create from template overlay
cy.getByTestID('add-resource-dropdown--button').click()
cy.getByTestID('add-resource-dropdown--template').click()
cy.getByTestID('task-import-template--overlay').within(() => {
cy.get('.cf-overlay--dismiss').click()
})
// TODO: extend to create a template from JSON
cy.getByTestID('add-resource-dropdown--button').click()
cy.getByTestID('add-resource-dropdown--import').click()
cy.getByTestID('task-import--overlay').within(() => {
cy.get('.cf-overlay--dismiss').click()
})
cy.getByTestID('add-resource-button').click()
})
// this test is broken due to a failure on the post route
it.skip('can create a task using http.post', () => {
@ -92,31 +80,6 @@ http.post(
.and('contain', taskName)
})
it('keeps user input in text area when attempting to import invalid JSON', () => {
cy.getByTestID('page-control-bar').within(() => {
cy.getByTestID('add-resource-dropdown--button').click()
})
cy.getByTestID('add-resource-dropdown--import').click()
cy.contains('Paste').click()
cy.getByTestID('import-overlay--textarea')
.click()
.type('this is invalid JSON')
cy.get('button[title*="Import JSON"]').click()
cy.getByTestID('import-overlay--textarea--error').should('have.length', 1)
cy.getByTestID('import-overlay--textarea').should($s =>
expect($s).to.contain('this is invalid JSON')
)
cy.getByTestID('import-overlay--textarea').type(
'{backspace}{backspace}{backspace}{backspace}{backspace}'
)
cy.get('button[title*="Import JSON"]').click()
cy.getByTestID('import-overlay--textarea--error').should('have.length', 1)
cy.getByTestID('import-overlay--textarea').should($s =>
expect($s).to.contain('this is invalid')
)
})
describe('When tasks already exist', () => {
beforeEach(() => {
cy.get('@org').then(({id}: Organization) => {
@ -395,11 +358,9 @@ function createFirstTask(
offset: string = '20m'
) {
cy.getByTestID('empty-tasks-list').within(() => {
cy.getByTestID('add-resource-dropdown--button').click()
cy.getByTestID('add-resource-button').click()
})
cy.getByTestID('add-resource-dropdown--new').click()
cy.get<Bucket>('@bucket').then(bucket => {
cy.getByTestID('flux-editor').within(() => {
cy.get('textarea.inputarea')

View File

@ -32,9 +32,7 @@ describe('Variables', () => {
'windowPeriod'
)
cy.getByTestID('add-resource-dropdown--button').click()
cy.getByTestID('add-resource-dropdown--new').click()
cy.getByTestID('add-resource-button').click()
cy.getByTestID('variable-type-dropdown--button').click()
cy.getByTestID('variable-type-dropdown-constant').click()
@ -145,9 +143,7 @@ describe('Variables', () => {
)
// Create a Map variable from scratch
cy.getByTestID('add-resource-dropdown--button').click()
cy.getByTestID('add-resource-dropdown--new').click()
cy.getByTestID('add-resource-button').click()
cy.getByTestID('variable-type-dropdown--button').click()
cy.getByTestID('variable-type-dropdown-map').click()
@ -171,9 +167,7 @@ describe('Variables', () => {
cy.getByTestID(`variable-card--name ${mapVariableName}`).should('exist')
// Create a Query variable from scratch
cy.getByTestID('add-resource-dropdown--button').click()
cy.getByTestID('add-resource-dropdown--new').click()
cy.getByTestID('add-resource-button').click()
cy.getByTestID('variable-type-dropdown--button').click()
cy.getByTestID('variable-type-dropdown-map').click()
@ -201,45 +195,6 @@ describe('Variables', () => {
cy.getByTestID(`variable-card--name ${queryVariableName}`).contains(
queryVariableName
)
//create variable by uploader
cy.getByTestID('add-resource-dropdown--button').click()
cy.getByTestID('add-resource-dropdown--import').click()
const yourFixturePath = 'data-for-variable.json'
cy.get('.drag-and-drop').attachFile(yourFixturePath, {
subjectType: 'drag-n-drop',
})
cy.getByTestID('submit-button Variable').click()
cy.getByTestID('resource-card variable')
.should('have.length', 4)
.contains('agent_host')
})
it('keeps user input in text area when attempting to import invalid JSON', () => {
cy.getByTestID('tabbed-page--header').within(() => {
cy.contains('Create').click()
})
cy.getByTestID('add-resource-dropdown--import').click()
cy.contains('Paste').click()
cy.getByTestID('import-overlay--textarea')
.click()
.type('this is invalid JSON')
cy.get('button[title*="Import JSON"]').click()
cy.getByTestID('import-overlay--textarea--error').should('have.length', 1)
cy.getByTestID('import-overlay--textarea').should($s =>
expect($s).to.contain('this is invalid JSON')
)
cy.getByTestID('import-overlay--textarea').type(
'{backspace}{backspace}{backspace}{backspace}{backspace}'
)
cy.get('button[title*="Import JSON"]').click()
cy.getByTestID('import-overlay--textarea--error').should('have.length', 1)
cy.getByTestID('import-overlay--textarea').contains('this is invalid')
})
it('can create and delete a label and filter a variable by label name & sort by variable name', () => {
@ -254,11 +209,7 @@ describe('Variables', () => {
cy.getByTestID('overlay--children').should('not.exist')
cy.getByTestID('add-resource-dropdown--button').click()
cy.getByTestID('add-resource-dropdown--new').should('have.length', 1)
cy.getByTestID('add-resource-dropdown--new').click()
cy.getByTestID('add-resource-button').click()
cy.getByTestID('variable-type-dropdown--button').click()
cy.getByTestID('variable-type-dropdown-constant').click()

View File

@ -6,7 +6,6 @@ import {push} from 'connected-react-router'
// APIs
import * as dashAPI from 'src/dashboards/apis'
import * as api from 'src/client'
import * as tempAPI from 'src/templates/api'
import {createCellWithView} from 'src/cells/actions/thunks'
// Schemas
@ -29,7 +28,6 @@ import {
updateTimeRangeFromQueryParams,
} from 'src/dashboards/actions/ranges'
import {getVariables, hydrateVariables} from 'src/variables/actions/thunks'
import {setExportTemplate} from 'src/templates/actions/creators'
import {checkDashboardLimits} from 'src/cloud/actions/limits'
import {setCells, Action as CellAction} from 'src/cells/actions/creators'
import {
@ -42,9 +40,6 @@ import {setLabelOnResource} from 'src/labels/actions/creators'
import * as creators from 'src/dashboards/actions/creators'
// Utils
import {filterUnusedVars} from 'src/shared/utils/filterUnusedVars'
import {dashboardToTemplate} from 'src/shared/utils/resourceToTemplate'
import {exportVariables} from 'src/variables/utils/exportVariables'
import {getSaveableView} from 'src/timeMachine/selectors'
import {incrementCloneName} from 'src/utils/naming'
import {isLimitError} from 'src/cloud/utils/limits'
@ -62,18 +57,14 @@ import {
GetState,
View,
Cell,
DashboardTemplate,
Label,
RemoteDataState,
DashboardEntities,
ViewEntities,
ResourceType,
VariableEntities,
Variable,
LabelEntities,
} from 'src/types'
import {CellsWithViewProperties} from 'src/client'
import {arrayOfVariables} from 'src/schemas/variables'
type Action = creators.Action
@ -291,37 +282,6 @@ export const getDashboards = () => async (
}
}
export const createDashboardFromTemplate = (
template: DashboardTemplate
) => async (dispatch, getState: GetState) => {
try {
const org = getOrg(getState())
await tempAPI.createDashboardFromTemplate(template, org.id)
const resp = await api.getDashboards({query: {orgID: org.id}})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
const dashboards = normalize<Dashboard, DashboardEntities, string[]>(
resp.data.dashboards,
arrayOfDashboards
)
dispatch(creators.setDashboards(RemoteDataState.Done, dashboards))
dispatch(notify(copy.importDashboardSucceeded()))
dispatch(checkDashboardLimits())
} catch (error) {
if (isLimitError(error)) {
dispatch(notify(copy.resourceLimitReached('dashboards')))
} else {
dispatch(notify(copy.importDashboardFailed(error)))
}
}
}
export const deleteDashboard = (dashboardID: string, name: string) => async (
dispatch
): Promise<void> => {
@ -495,70 +455,6 @@ export const removeDashboardLabel = (
}
}
export const convertToTemplate = (dashboardID: string) => async (
dispatch,
getState: GetState
): Promise<void> => {
try {
dispatch(setExportTemplate(RemoteDataState.Loading))
const state = getState()
const org = getOrg(state)
const dashResp = await api.getDashboard({dashboardID})
if (dashResp.status !== 200) {
throw new Error(dashResp.data.message)
}
const {entities, result} = normalize<Dashboard, DashboardEntities, string>(
dashResp.data,
dashboardSchema
)
const dashboard = entities.dashboards[result]
const cells = dashboard.cells.map(cellID => entities.cells[cellID])
const pendingViews = dashboard.cells.map(cellID =>
dashAPI.getView(dashboardID, cellID)
)
const views = await Promise.all(pendingViews)
const resp = await api.getVariables({query: {orgID: org.id}})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
let vars = []
// dumb bug
// https://github.com/paularmstrong/normalizr/issues/290
if (resp.data.variables.length) {
const normVars = normalize<Variable, VariableEntities, string>(
resp.data.variables,
arrayOfVariables
)
vars = Object.values(normVars.entities.variables)
}
const variables = filterUnusedVars(vars, views)
const exportedVariables = exportVariables(variables, vars)
const dashboardTemplate = dashboardToTemplate(
state,
dashboard,
cells,
views,
exportedVariables
)
dispatch(setExportTemplate(RemoteDataState.Done, dashboardTemplate))
} catch (error) {
console.error(error)
dispatch(setExportTemplate(RemoteDataState.Error))
dispatch(notify(copy.createTemplateFailed(error)))
}
}
export const saveVEOView = (dashboardID: string) => async (
dispatch,
getState: GetState

View File

@ -1,78 +0,0 @@
import React, {PureComponent} from 'react'
import {connect, ConnectedProps} from 'react-redux'
import {withRouter, RouteComponentProps} from 'react-router-dom'
// Components
import ExportOverlay from 'src/shared/components/ExportOverlay'
// Actions
import {convertToTemplate as convertToTemplateAction} from 'src/dashboards/actions/thunks'
import {clearExportTemplate as clearExportTemplateAction} from 'src/templates/actions/thunks'
// Types
import {AppState} from 'src/types'
import {
dashboardCopySuccess,
dashboardCopyFailed,
} from 'src/shared/copy/notifications'
type ReduxProps = ConnectedProps<typeof connector>
type Props = ReduxProps &
RouteComponentProps<{orgID: string; dashboardID: string}>
class DashboardExportOverlay extends PureComponent<Props> {
public componentDidMount() {
const {
match: {
params: {dashboardID},
},
convertToTemplate,
} = this.props
convertToTemplate(dashboardID)
}
public render() {
const {status, dashboardTemplate} = this.props
const notes = (_text, success) => {
if (success) {
return dashboardCopySuccess()
}
return dashboardCopyFailed()
}
return (
<ExportOverlay
resourceName="Dashboard"
resource={dashboardTemplate}
onDismissOverlay={this.onDismiss}
onCopyText={notes}
status={status}
/>
)
}
private onDismiss = () => {
const {history, clearExportTemplate} = this.props
history.goBack()
clearExportTemplate()
}
}
const mstp = (state: AppState) => ({
dashboardTemplate: state.resources.templates.exportTemplate.item,
status: state.resources.templates.exportTemplate.status,
})
const mdtp = {
convertToTemplate: convertToTemplateAction,
clearExportTemplate: clearExportTemplateAction,
}
const connector = connect(mstp, mdtp)
export default connector(withRouter(DashboardExportOverlay))

View File

@ -1,90 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import {isEmpty} from 'lodash'
import {connect, ConnectedProps} from 'react-redux'
// Components
import ImportOverlay from 'src/shared/components/ImportOverlay'
// Copy
import {invalidJSON} from 'src/shared/copy/notifications'
// Actions
import {
getDashboards,
createDashboardFromTemplate as createDashboardFromTemplateAction,
} from 'src/dashboards/actions/thunks'
import {notify as notifyAction} from 'src/shared/actions/notifications'
// Types
import {ComponentStatus} from '@influxdata/clockface'
// Utils
import jsonlint from 'jsonlint-mod'
interface State {
status: ComponentStatus
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = RouteComponentProps<{orgID: string}> & ReduxProps
class DashboardImportOverlay extends PureComponent<Props> {
public state: State = {
status: ComponentStatus.Default,
}
public render() {
return (
<ImportOverlay
isVisible={true}
onDismissOverlay={this.onDismiss}
resourceName="Dashboard"
onSubmit={this.handleImportDashboard}
status={this.state.status}
updateStatus={this.updateOverlayStatus}
/>
)
}
private updateOverlayStatus = (status: ComponentStatus) =>
this.setState(() => ({status}))
private handleImportDashboard = (uploadContent: string) => {
const {createDashboardFromTemplate, notify, populateDashboards} = this.props
let template
this.updateOverlayStatus(ComponentStatus.Default)
try {
template = jsonlint.parse(uploadContent)
} catch (error) {
this.updateOverlayStatus(ComponentStatus.Error)
notify(invalidJSON(error.message))
return
}
if (isEmpty(template)) {
this.onDismiss()
}
createDashboardFromTemplate(template)
populateDashboards()
this.onDismiss()
}
private onDismiss = (): void => {
const {history} = this.props
history.goBack()
}
}
const mdtp = {
notify: notifyAction,
populateDashboards: getDashboards,
createDashboardFromTemplate: createDashboardFromTemplateAction,
}
const connector = connect(null, mdtp)
export default connector(withRouter(DashboardImportOverlay))

View File

@ -1,91 +0,0 @@
.import-template-overlay,
.import-template-overlay--empty {
height: 500px;
display: flex;
}
.import-template-overlay {
align-items: stretch;
}
.import-template-overlay--templates {
flex: 2 0 0;
border-radius: $ix-radius;
}
.import-template-overlay--details {
flex: 5 0 0;
margin-left: $ix-marg-b;
}
.import-template-overlay--panel {
min-height: 500px;
}
.import-template-overlay--name {
margin-top: 0;
margin-bottom: $ix-marg-b;
}
.import-template-overlay--description {
margin-top: 0;
margin-bottom: $ix-marg-d;
}
.import-template-overlay--heading {
margin-top: 0;
border-bottom: $ix-border solid $g5-pepper;
padding-bottom: $ix-marg-b;
}
.import-templates-overlay--included {
margin-bottom: 0;
}
.import-template-overlay--name.missing,
.import-template-overlay--included.missing,
.import-template-overlay--description.missing {
font-style: italic;
color: $g9-mountain;
}
.import-template-overlay--empty {
background-color: $g3-castle;
border-radius: $ix-radius;
align-content: center;
align-items: center;
justify-content: center;
}
.import-template-overlay--template {
user-select: none;
border-radius: $ix-radius;
padding: $ix-marg-b;
background-color: $g1-raven;
margin-bottom: $ix-border;
border: $ix-border solid $g1-raven;
color: $g11-sidewalk;
display: flex;
flex-wrap: none;
align-items: center;
transition: color 0.25s ease, background-color 0.25s ease, border-color 0.25s ease;
&:hover {
border-color: $g5-pepper;
background-color: $g5-pepper;
color: $g18-cloud;
cursor: pointer;
}
&.active {
border-color: $c-rainforest;
color: $g18-cloud;
background-color: $g5-pepper;
}
}
.import-template-overlay--list-label {
font-size: 13px;
font-weight: 500;
margin-left: $ix-marg-b;
}

View File

@ -98,13 +98,6 @@ class DashboardCard extends PureComponent<Props> {
private get contextMenu(): JSX.Element {
return (
<Context>
<Context.Menu icon={IconFont.CogThick}>
<Context.Item
label="Export"
action={this.handleExport}
testID="context-menu-item-export"
/>
</Context.Menu>
<Context.Menu
icon={IconFont.Duplicate}
color={ComponentColor.Secondary}
@ -171,18 +164,6 @@ class DashboardCard extends PureComponent<Props> {
onRemoveDashboardLabel(id, label)
}
private handleExport = () => {
const {
history,
match: {
params: {orgID},
},
id,
} = this.props
history.push(`/orgs/${orgID}/dashboards-list/${id}/export`)
}
}
const mdtp = {

View File

@ -2,7 +2,6 @@
import React, {PureComponent} from 'react'
import {RouteComponentProps} from 'react-router-dom'
import {connect, ConnectedProps} from 'react-redux'
import {Switch, Route} from 'react-router-dom'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
@ -11,13 +10,10 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
import DashboardsIndexContents from 'src/dashboards/components/dashboard_index/DashboardsIndexContents'
import {Page} from '@influxdata/clockface'
import SearchWidget from 'src/shared/components/search_widget/SearchWidget'
import AddResourceDropdown from 'src/shared/components/AddResourceDropdown'
import AddResourceButton from 'src/shared/components/AddResourceButton'
import GetAssetLimits from 'src/cloud/components/GetAssetLimits'
import RateLimitAlert from 'src/cloud/components/RateLimitAlert'
import ResourceSortDropdown from 'src/shared/components/resource_sort_dropdown/ResourceSortDropdown'
import DashboardImportOverlay from 'src/dashboards/components/DashboardImportOverlay'
import CreateFromTemplateOverlay from 'src/templates/components/createFromTemplateOverlay/CreateFromTemplateOverlay'
import DashboardExportOverlay from 'src/dashboards/components/DashboardExportOverlay'
// Utils
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
@ -80,12 +76,9 @@ class DashboardIndex extends PureComponent<Props, State> {
/>
</Page.ControlBarLeft>
<Page.ControlBarRight>
<AddResourceDropdown
<AddResourceButton
onSelectNew={createDashboard}
onSelectImport={this.summonImportOverlay}
onSelectTemplate={this.summonImportFromTemplateOverlay}
resourceName="Dashboard"
canImportFromTemplate={true}
limitStatus={limitStatus}
/>
</Page.ControlBarRight>
@ -106,20 +99,6 @@ class DashboardIndex extends PureComponent<Props, State> {
</GetAssetLimits>
</Page.Contents>
</Page>
<Switch>
<Route
path="/orgs/:orgID/dashboards-list/:dashboardID/export"
component={DashboardExportOverlay}
/>
<Route
path="/orgs/:orgID/dashboards-list/import/template"
component={CreateFromTemplateOverlay}
/>
<Route
path="/orgs/:orgID/dashboards-list/import"
component={DashboardImportOverlay}
/>
</Switch>
</>
)
}
@ -135,26 +114,6 @@ class DashboardIndex extends PureComponent<Props, State> {
private handleFilterDashboards = (searchTerm: string): void => {
this.setState({searchTerm})
}
private summonImportOverlay = (): void => {
const {
history,
match: {
params: {orgID},
},
} = this.props
history.push(`/orgs/${orgID}/dashboards-list/import`)
}
private summonImportFromTemplateOverlay = (): void => {
const {
history,
match: {
params: {orgID},
},
} = this.props
history.push(`/orgs/${orgID}/dashboards-list/import/template`)
}
}
const mstp = (state: AppState) => {

View File

@ -3,7 +3,7 @@ import React, {FC} from 'react'
// Components
import {EmptyState, ComponentSize} from '@influxdata/clockface'
import AddResourceDropdown from 'src/shared/components/AddResourceDropdown'
import AddResourceButton from 'src/shared/components/AddResourceButton'
// Actions
import {createDashboard} from 'src/dashboards/actions/thunks'
@ -11,15 +11,11 @@ import {createDashboard} from 'src/dashboards/actions/thunks'
interface ComponentProps {
searchTerm?: string
onCreateDashboard: typeof createDashboard
summonImportOverlay: () => void
summonImportFromTemplateOverlay: () => void
}
const DashboardsTableEmpty: FC<ComponentProps> = ({
searchTerm,
onCreateDashboard,
summonImportOverlay,
summonImportFromTemplateOverlay,
}) => {
if (searchTerm) {
return (
@ -34,12 +30,9 @@ const DashboardsTableEmpty: FC<ComponentProps> = ({
<EmptyState.Text>
Looks like you don't have any <b>Dashboards</b>, why not create one?
</EmptyState.Text>
<AddResourceDropdown
<AddResourceButton
onSelectNew={onCreateDashboard}
onSelectImport={summonImportOverlay}
onSelectTemplate={summonImportFromTemplateOverlay}
resourceName="Dashboard"
canImportFromTemplate={true}
/>
</EmptyState>
)

View File

@ -1,7 +1,6 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect, ConnectedProps} from 'react-redux'
import {withRouter, RouteComponentProps} from 'react-router-dom'
// Components
import DashboardCards from 'src/dashboards/components/dashboard_index/DashboardCards'
@ -30,7 +29,7 @@ interface OwnProps {
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = OwnProps & ReduxProps & RouteComponentProps<{orgID: string}>
type Props = OwnProps & ReduxProps
class DashboardsTable extends PureComponent<Props> {
public componentDidMount() {
@ -55,8 +54,6 @@ class DashboardsTable extends PureComponent<Props> {
<DashboardsTableEmpty
searchTerm={searchTerm}
onCreateDashboard={onCreateDashboard}
summonImportFromTemplateOverlay={this.summonImportFromTemplateOverlay}
summonImportOverlay={this.summonImportOverlay}
/>
)
}
@ -71,26 +68,6 @@ class DashboardsTable extends PureComponent<Props> {
/>
)
}
private summonImportOverlay = (): void => {
const {
history,
match: {
params: {orgID},
},
} = this.props
history.push(`/orgs/${orgID}/dashboards-list/import`)
}
private summonImportFromTemplateOverlay = (): void => {
const {
history,
match: {
params: {orgID},
},
} = this.props
history.push(`/orgs/${orgID}/dashboards-list/import/template`)
}
}
const mstp = (state: AppState) => {
@ -109,4 +86,4 @@ const mdtp = {
const connector = connect(mstp, mdtp)
export default connector(withRouter(DashboardsTable))
export default connector(DashboardsTable)

View File

@ -15,7 +15,6 @@ import {getPlugins} from 'src/dataLoaders/actions/telegrafEditor'
import {getScrapers} from 'src/scrapers/actions/thunks'
import {getTasks} from 'src/tasks/actions/thunks'
import {getTelegrafs} from 'src/telegrafs/actions/thunks'
import {getTemplates} from 'src/templates/actions/thunks'
import {getVariables} from 'src/variables/actions/thunks'
//Utils
@ -100,10 +99,6 @@ class GetResources extends PureComponent<Props> {
return this.props.getAuthorizations()
}
case ResourceType.Templates: {
return this.props.getTemplates()
}
case ResourceType.Members: {
return this.props.getMembers()
}
@ -158,7 +153,6 @@ const mdtp = {
getAuthorizations: getAuthorizations,
getDashboards: getDashboards,
getTasks: getTasks,
getTemplates: getTemplates,
getMembers: getMembers,
getChecks: getChecks,
getNotificationRules: getNotificationRules,

View File

@ -6,6 +6,9 @@ import {withRouter, RouteComponentProps} from 'react-router-dom'
// Components
import TabbedPageTabs from 'src/shared/tabbedPage/TabbedPageTabs'
// Utils
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
// Types
import {TabbedPageTab} from 'src/shared/tabbedPage/TabbedPageTabs'
@ -43,9 +46,13 @@ class SettingsNavigation extends PureComponent<Props> {
},
]
const displayedTabs = isFlagEnabled('communityTemplates')
? tabs
: tabs.filter(t => t.id !== 'templates')
return (
<TabbedPageTabs
tabs={tabs}
tabs={displayedTabs}
activeTab={activeTab}
onTabClick={handleTabClick}
/>

View File

@ -18,7 +18,6 @@ export enum ActionTypes {
SetNotebookMiniMapState = 'SET_NOTEBOOK_MINI_MAP_STATE',
SetAutoRefresh = 'SET_AUTOREFRESH',
SetTimeZone = 'SET_APP_TIME_ZONE',
TemplateControlBarVisibilityToggled = 'TemplateControlBarVisibilityToggledAction',
Noop = 'NOOP',
}

View File

@ -0,0 +1,70 @@
// Libraries
import React, {FC} from 'react'
import {connect, ConnectedProps} from 'react-redux'
import _ from 'lodash'
// Components
import {
IconFont,
ComponentColor,
ComponentSize,
Button,
ComponentStatus,
} from '@influxdata/clockface'
// Actions
import {showOverlay, dismissOverlay} from 'src/overlays/actions/overlays'
// Types
import {LimitStatus} from 'src/cloud/actions/limits'
// Constants
import {CLOUD} from 'src/shared/constants'
interface Props {
onSelectNew: () => void
resourceName: string
status?: ComponentStatus
limitStatus?: LimitStatus
}
type ReduxProps = ConnectedProps<typeof connector>
const AddResourceButton: FC<Props & ReduxProps> = ({
resourceName,
onSelectNew,
onShowOverlay,
onDismissOverlay,
limitStatus = LimitStatus.OK,
status = ComponentStatus.Default,
}) => {
const showLimitOverlay = () =>
onShowOverlay('asset-limit', {asset: `${resourceName}s`}, onDismissOverlay)
const onClick =
CLOUD && limitStatus === LimitStatus.EXCEEDED
? showLimitOverlay
: onSelectNew
return (
<Button
style={{width: '190px'}}
testID="add-resource-button"
onClick={onClick}
color={ComponentColor.Primary}
size={ComponentSize.Small}
text={`Create ${resourceName}`}
icon={IconFont.Plus}
status={status}
/>
)
}
const mdtp = {
onShowOverlay: showOverlay,
onDismissOverlay: dismissOverlay,
}
const connector = connect(null, mdtp)
export default connector(AddResourceButton)

View File

@ -1,174 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect, ConnectedProps} from 'react-redux'
import _ from 'lodash'
// Components
import {
IconFont,
ComponentColor,
ComponentSize,
Dropdown,
ComponentStatus,
} from '@influxdata/clockface'
// Actions
import {showOverlay, dismissOverlay} from 'src/overlays/actions/overlays'
// Types
import {LimitStatus} from 'src/cloud/actions/limits'
// Constants
import {CLOUD} from 'src/shared/constants'
interface OwnProps {
onSelectNew: () => void
onSelectImport: () => void
onSelectTemplate?: () => void
resourceName: string
limitStatus?: LimitStatus
}
interface DefaultProps {
canImportFromTemplate: boolean
status: ComponentStatus
titleText: string
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = OwnProps & DefaultProps & ReduxProps
class AddResourceDropdown extends PureComponent<Props> {
public static defaultProps: DefaultProps = {
canImportFromTemplate: false,
status: ComponentStatus.Default,
titleText: null,
}
public render() {
const {titleText, status} = this.props
return (
<Dropdown
style={{width: '190px'}}
testID="add-resource-dropdown"
button={(active, onClick) => (
<Dropdown.Button
testID="add-resource-dropdown--button"
active={active}
onClick={onClick}
color={ComponentColor.Primary}
size={ComponentSize.Small}
icon={IconFont.Plus}
status={status}
>
{titleText || `Create ${this.props.resourceName}`}
</Dropdown.Button>
)}
menu={onCollapse => (
<Dropdown.Menu
onCollapse={onCollapse}
testID="add-resource-dropdown--menu"
>
{this.optionItems}
</Dropdown.Menu>
)}
>
{this.optionItems}
</Dropdown>
)
}
private get optionItems(): JSX.Element[] {
const importOption = this.importOption
const newOption = this.newOption
const templateOption = this.templateOption
const items = [
<Dropdown.Item
id={newOption}
key={newOption}
onClick={this.handleSelect}
value={newOption}
testID="add-resource-dropdown--new"
>
{newOption}
</Dropdown.Item>,
<Dropdown.Item
id={importOption}
key={importOption}
onClick={this.handleSelect}
value={importOption}
testID="add-resource-dropdown--import"
>
{importOption}
</Dropdown.Item>,
]
if (!!this.props.canImportFromTemplate) {
items.push(
<Dropdown.Item
id={templateOption}
key={templateOption}
onClick={this.handleSelect}
value={templateOption}
testID="add-resource-dropdown--template"
>
{templateOption}
</Dropdown.Item>
)
}
return items
}
private get newOption(): string {
return `New ${this.props.resourceName}`
}
private get importOption(): string {
return `Import ${this.props.resourceName}`
}
private get templateOption(): string {
return `From a Template`
}
private handleLimit = (): void => {
const {resourceName, onShowOverlay, onDismissOverlay} = this.props
onShowOverlay('asset-limit', {asset: `${resourceName}s`}, onDismissOverlay)
}
private handleSelect = (selection: string): void => {
const {
onSelectNew,
onSelectImport,
onSelectTemplate,
limitStatus = LimitStatus.OK,
} = this.props
if (CLOUD && limitStatus === LimitStatus.EXCEEDED) {
this.handleLimit()
return
}
if (selection === this.newOption) {
onSelectNew()
}
if (selection === this.importOption) {
onSelectImport()
}
if (selection == this.templateOption) {
onSelectTemplate()
}
}
}
const mdtp = {
onShowOverlay: showOverlay,
onDismissOverlay: dismissOverlay,
}
const connector = connect(null, mdtp)
export default connector(AddResourceDropdown)

View File

@ -1,3 +0,0 @@
.export-overlay--text-area {
height: 60vh;
}

View File

@ -1,165 +0,0 @@
import React, {PureComponent} from 'react'
import {connect, ConnectedProps} from 'react-redux'
import {get} from 'lodash'
// Components
import {
Form,
Button,
SpinnerContainer,
TechnoSpinner,
Overlay,
} from '@influxdata/clockface'
import {Controlled as ReactCodeMirror} from 'react-codemirror2'
import CopyButton from 'src/shared/components/CopyButton'
// Actions
import {createTemplateFromResource} from 'src/templates/actions/thunks'
// Utils
import {downloadTextFile} from 'src/shared/utils/download'
// Types
import {DocumentCreate} from '@influxdata/influx'
import {ComponentColor, ComponentSize} from '@influxdata/clockface'
import {RemoteDataState, Notification} from 'src/types'
interface OwnProps {
onDismissOverlay: () => void
resource: DocumentCreate
resourceName: string
onCopyText?: (text: string, status: boolean) => Notification
status: RemoteDataState
isVisible: boolean
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = OwnProps & ReduxProps
class ExportOverlay extends PureComponent<Props> {
public static defaultProps = {
isVisible: true,
}
public render() {
const {isVisible, resourceName, onDismissOverlay, status} = this.props
return (
<Overlay visible={isVisible}>
<Overlay.Container maxWidth={800}>
<Form onSubmit={this.handleExport}>
<Overlay.Header
title={`Export ${resourceName}`}
onDismiss={onDismissOverlay}
/>
<Overlay.Body>
<SpinnerContainer
loading={status}
spinnerComponent={<TechnoSpinner />}
>
{this.overlayBody}
</SpinnerContainer>
</Overlay.Body>
<Overlay.Footer>
{this.downloadButton}
{this.toTemplateButton}
{this.copyButton}
</Overlay.Footer>
</Form>
</Overlay.Container>
</Overlay>
)
}
private doNothing = () => {}
private get overlayBody(): JSX.Element {
const options = {
tabIndex: 1,
mode: 'json',
readonly: true,
lineNumbers: true,
autoRefresh: true,
theme: 'time-machine',
completeSingle: false,
}
return (
<div
className="export-overlay--text-area"
data-testid="export-overlay--text-area"
>
<ReactCodeMirror
autoFocus={false}
autoCursor={true}
value={this.resourceText}
options={options}
onBeforeChange={this.doNothing}
onTouchStart={this.doNothing}
/>
</div>
)
}
private get resourceText(): string {
return JSON.stringify(this.props.resource, null, 1)
}
private get copyButton(): JSX.Element {
return (
<CopyButton
textToCopy={this.resourceText}
contentName={this.props.resourceName}
onCopyText={this.props.onCopyText}
size={ComponentSize.Small}
color={ComponentColor.Secondary}
/>
)
}
private get downloadButton(): JSX.Element {
return (
<Button
text="Download JSON"
onClick={this.handleExport}
color={ComponentColor.Primary}
/>
)
}
private get toTemplateButton(): JSX.Element {
return (
<Button
text="Save as template"
onClick={this.handleConvertToTemplate}
color={ComponentColor.Primary}
/>
)
}
private handleExport = (): void => {
const {resource, resourceName, onDismissOverlay} = this.props
const name = get(resource, 'content.data.attributes.name', resourceName)
downloadTextFile(JSON.stringify(resource, null, 1), name, '.json')
onDismissOverlay()
}
private handleConvertToTemplate = () => {
const {
resource,
onDismissOverlay,
resourceName,
onCreateTemplateFromResource,
} = this.props
onCreateTemplateFromResource(resource, resourceName)
onDismissOverlay()
}
}
const mdtp = {
onCreateTemplateFromResource: createTemplateFromResource,
}
const connector = connect(null, mdtp)
export default connector(ExportOverlay)

View File

@ -1,12 +0,0 @@
@import "src/style/modules";
.import--options {
display: flex;
margin-top: -8px;
margin-bottom: $ix-marg-b;
justify-content: center;
}
.import--dropdown {
margin-top: $ix-marg-b;
}

View File

@ -1,192 +0,0 @@
// Libraries
import React, {PureComponent, ChangeEvent} from 'react'
import {withRouter, RouteComponentProps} from 'react-router-dom'
// Components
import {
Form,
SelectGroup,
Button,
TextArea,
Overlay,
} from '@influxdata/clockface'
import DragAndDrop from 'src/shared/components/DragAndDrop'
// Types
import {
ButtonType,
ComponentColor,
ComponentStatus,
} from '@influxdata/clockface'
enum ImportOption {
Upload = 'upload',
Paste = 'paste',
}
interface OwnProps {
onDismissOverlay: () => void
resourceName: string
onSubmit: (importString: string, orgID: string) => void
isVisible?: boolean
status?: ComponentStatus
updateStatus?: (status: ComponentStatus) => void
}
interface State {
selectedImportOption: ImportOption
importContent: string
}
type Props = OwnProps & RouteComponentProps<{orgID: string}>
class ImportOverlay extends PureComponent<Props, State> {
public static defaultProps: {isVisible: boolean} = {
isVisible: true,
}
public state: State = {
selectedImportOption: ImportOption.Upload,
importContent: '',
}
public render() {
const {isVisible, resourceName} = this.props
const {selectedImportOption} = this.state
return (
<Overlay visible={isVisible} testID="task-import--overlay">
<Overlay.Container maxWidth={800}>
<Form onSubmit={this.submit}>
<Overlay.Header
title={`Import ${resourceName}`}
onDismiss={this.onDismiss}
/>
<Overlay.Body>
<div className="import--options">
<SelectGroup>
<SelectGroup.Option
name="import-mode"
id={ImportOption.Upload}
active={selectedImportOption === ImportOption.Upload}
value={ImportOption.Upload}
onClick={this.handleSetImportOption}
titleText="Upload"
>
Upload File
</SelectGroup.Option>
<SelectGroup.Option
name="import-mode"
id={ImportOption.Paste}
active={selectedImportOption === ImportOption.Paste}
value={ImportOption.Paste}
onClick={this.handleSetImportOption}
titleText="Paste"
>
Paste JSON
</SelectGroup.Option>
</SelectGroup>
</div>
{this.importBody}
</Overlay.Body>
<Overlay.Footer>{this.submitButton}</Overlay.Footer>
</Form>
</Overlay.Container>
</Overlay>
)
}
private get importBody(): JSX.Element {
const {selectedImportOption, importContent} = this.state
const {status = ComponentStatus.Default} = this.props
if (selectedImportOption === ImportOption.Upload) {
return (
<DragAndDrop
submitText="Upload"
handleSubmit={this.handleSetImportContent}
submitOnDrop={true}
submitOnUpload={true}
onCancel={this.clearImportContent}
/>
)
}
if (selectedImportOption === ImportOption.Paste) {
return (
<TextArea
status={status}
value={importContent}
onChange={this.handleChangeTextArea}
testID="import-overlay--textarea"
/>
)
}
}
private handleChangeTextArea = (
e: ChangeEvent<HTMLTextAreaElement>
): void => {
const {updateStatus = () => {}} = this.props
const importContent = e.target.value
this.handleSetImportContent(importContent)
updateStatus(ComponentStatus.Default)
}
private get submitButton(): JSX.Element {
const {resourceName} = this.props
const {selectedImportOption, importContent} = this.state
const isEnabled =
selectedImportOption === ImportOption.Paste ||
(selectedImportOption === ImportOption.Upload && importContent)
const status = isEnabled
? ComponentStatus.Default
: ComponentStatus.Disabled
return (
<Button
text={`Import JSON as ${resourceName}`}
testID={`submit-button ${resourceName}`}
color={ComponentColor.Primary}
status={status}
type={ButtonType.Submit}
/>
)
}
private submit = () => {
const {importContent} = this.state
const {
onSubmit,
match: {
params: {orgID},
},
} = this.props
onSubmit(importContent, orgID)
this.clearImportContent()
}
private clearImportContent = () => {
this.setState((state, props) => {
const {status = ComponentStatus.Default} = props
return status === ComponentStatus.Error ? {...state} : {importContent: ''}
})
}
private onDismiss = () => {
const {onDismissOverlay} = this.props
this.clearImportContent()
onDismissOverlay()
}
private handleSetImportOption = (selectedImportOption: ImportOption) => {
this.clearImportContent()
this.setState({selectedImportOption})
}
private handleSetImportContent = (importContent: string) => {
this.setState({importContent})
}
}
export default withRouter(ImportOverlay)

View File

@ -1,108 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
// Components
import {
Button,
SpinnerContainer,
TechnoSpinner,
Overlay,
ComponentSize,
} from '@influxdata/clockface'
import {Controlled as ReactCodeMirror} from 'react-codemirror2'
import CopyButton from 'src/shared/components/CopyButton'
// Types
import {ComponentColor} from '@influxdata/clockface'
import {RemoteDataState, DashboardTemplate} from 'src/types'
import {DocumentCreate} from '@influxdata/influx'
interface Props {
onDismissOverlay: () => void
resource: DashboardTemplate | DocumentCreate
overlayHeading: string
status: RemoteDataState
isVisible: boolean
}
export default class ViewOverlay extends PureComponent<Props> {
public static defaultProps = {
isVisible: true,
}
public render() {
const {isVisible, overlayHeading, onDismissOverlay, status} = this.props
return (
<Overlay visible={isVisible}>
<Overlay.Container maxWidth={800}>
<Overlay.Header title={overlayHeading} onDismiss={onDismissOverlay} />
<Overlay.Body>
<SpinnerContainer
loading={status}
spinnerComponent={<TechnoSpinner />}
>
{this.overlayBody}
</SpinnerContainer>
</Overlay.Body>
<Overlay.Footer>
{this.closeButton}
{this.copyButton}
</Overlay.Footer>
</Overlay.Container>
</Overlay>
)
}
private doNothing = () => {}
private get overlayBody(): JSX.Element {
const options = {
tabIndex: 1,
mode: 'json',
readonly: true,
lineNumbers: true,
autoRefresh: true,
theme: 'time-machine',
completeSingle: false,
}
return (
<div className="export-overlay--text-area">
<ReactCodeMirror
autoFocus={false}
autoCursor={true}
value={this.resourceText}
options={options}
onBeforeChange={this.doNothing}
onTouchStart={this.doNothing}
/>
</div>
)
}
private get resourceText(): string {
return JSON.stringify(this.props.resource, null, 1)
}
private get copyButton(): JSX.Element {
return (
<CopyButton
textToCopy={this.resourceText}
contentName={this.props.overlayHeading}
size={ComponentSize.Small}
color={ComponentColor.Secondary}
/>
)
}
private get closeButton(): JSX.Element {
return (
<Button
text="Close"
onClick={this.props.onDismissOverlay}
color={ComponentColor.Primary}
/>
)
}
}

View File

@ -6,7 +6,6 @@ import {
Task,
Variable,
Label,
Template,
Bucket,
Telegraf,
Scraper,
@ -17,7 +16,6 @@ export type DashboardSortKey = keyof Dashboard | 'meta.updatedAt'
export type TaskSortKey = keyof Task
export type VariableSortKey = keyof Variable | 'arguments.type'
export type LabelSortKey = keyof Label | 'properties.description'
export type TemplateSortKey = keyof Template | 'meta.name' | 'meta.description'
export type BucketSortKey = keyof Bucket | 'retentionRules[0].everySeconds'
export type TelegrafSortKey = keyof Telegraf
export type ScraperSortKey = keyof Scraper
@ -28,7 +26,6 @@ export type SortKey =
| TaskSortKey
| VariableSortKey
| LabelSortKey
| TemplateSortKey
| BucketSortKey
| TelegrafSortKey
| ScraperSortKey
@ -177,33 +174,6 @@ export const generateSortItems = (
sortDirection: Sort.Descending,
},
]
case ResourceType.Templates:
return [
{
label: 'Name (A → Z)',
sortKey: 'meta.name',
sortType: SortTypes.String,
sortDirection: Sort.Ascending,
},
{
label: 'Name (Z → A)',
sortKey: 'meta.name',
sortType: SortTypes.String,
sortDirection: Sort.Descending,
},
{
label: 'Description (A → Z)',
sortKey: 'meta.description',
sortType: SortTypes.String,
sortDirection: Sort.Ascending,
},
{
label: 'Description (Z → A)',
sortKey: 'meta.description',
sortType: SortTypes.String,
sortDirection: Sort.Descending,
},
]
case ResourceType.Buckets:
return [
{

View File

@ -19,7 +19,7 @@ import TelegrafsPage from 'src/telegrafs/containers/TelegrafsPage'
import ScrapersIndex from 'src/scrapers/containers/ScrapersIndex'
import WriteDataPage from 'src/writeData/containers/WriteDataPage'
import VariablesIndex from 'src/variables/containers/VariablesIndex'
import TemplatesIndex from 'src/templates/containers/TemplatesIndex'
import {CommunityTemplatesIndex} from 'src/templates/containers/CommunityTemplatesIndex'
import LabelsIndex from 'src/labels/containers/LabelsIndex'
import OrgProfilePage from 'src/organizations/containers/OrgProfilePage'
import AlertingIndex from 'src/alerting/components/AlertingIndex'
@ -181,10 +181,14 @@ const SetOrg: FC<Props> = ({
path={`${orgPath}/settings/variables`}
component={VariablesIndex}
/>
<Route
path={`${orgPath}/settings/templates`}
component={TemplatesIndex}
/>
{isFlagEnabled('communityTemplates') && (
<Route
path={`${orgPath}/settings/templates`}
component={CommunityTemplatesIndex}
/>
)}
<Route
exact
path={`${orgPath}/settings/labels`}

View File

@ -310,17 +310,7 @@ export const copyToClipboardFailed = (
message: `${title}'${text}' was not copied to clipboard.`,
})
// Templates
export const addTemplateLabelFailed = (): Notification => ({
...defaultErrorNotification,
message: 'Failed to add label to template',
})
export const removeTemplateLabelFailed = (): Notification => ({
...defaultErrorNotification,
message: 'Failed to remove label from template',
})
//Templates
export const TelegrafDashboardCreated = (configs: string[]): Notification => ({
...defaultSuccessNotification,
message: `Successfully created dashboards for telegraf plugin${
@ -333,41 +323,6 @@ export const TelegrafDashboardFailed = (): Notification => ({
message: `Could not create dashboards for one or more plugins`,
})
export const importTaskSucceeded = (): Notification => ({
...defaultSuccessNotification,
message: `Successfully imported task.`,
})
export const importTaskFailed = (error: string): Notification => ({
...defaultErrorNotification,
message: `Failed to import task: ${error}`,
})
export const importDashboardSucceeded = (): Notification => ({
...defaultSuccessNotification,
message: `Successfully imported dashboard.`,
})
export const importDashboardFailed = (error: string): Notification => ({
...defaultErrorNotification,
message: `Failed to import dashboard: ${error}`,
})
export const importTemplateSucceeded = (): Notification => ({
...defaultSuccessNotification,
message: `Successfully imported template.`,
})
export const importTemplateFailed = (error: string): Notification => ({
...defaultErrorNotification,
message: `Failed to import template: ${error}`,
})
export const createTemplateFailed = (error: string): Notification => ({
...defaultErrorNotification,
message: `Failed to resource as template: ${error}`,
})
export const createResourceFromTemplateFailed = (
error: string
): Notification => ({
@ -375,51 +330,6 @@ export const createResourceFromTemplateFailed = (
message: `Failed to create from template: ${error}`,
})
export const updateTemplateSucceeded = (): Notification => ({
...defaultSuccessNotification,
message: `Successfully updated template.`,
})
export const updateTemplateFailed = (error: string): Notification => ({
...defaultErrorNotification,
message: `Failed to update template: ${error}`,
})
export const deleteTemplateFailed = (error: string): Notification => ({
...defaultErrorNotification,
message: `Failed to delete template: ${error}`,
})
export const deleteTemplateSuccess = (): Notification => ({
...defaultSuccessNotification,
message: 'Template was deleted successfully',
})
export const cloneTemplateFailed = (error: string): Notification => ({
...defaultErrorNotification,
message: `Failed to clone template: ${error}`,
})
export const cloneTemplateSuccess = (): Notification => ({
...defaultSuccessNotification,
message: 'Template cloned successfully',
})
export const resourceSavedAsTemplate = (
resourceName: string
): Notification => ({
...defaultSuccessNotification,
message: `Successfully saved ${resourceName.toLowerCase()} as template.`,
})
export const saveResourceAsTemplateFailed = (
resourceName: string,
error: string
): Notification => ({
...defaultErrorNotification,
message: `Failed to save ${resourceName.toLowerCase()} as template: ${error}`,
})
// Labels
export const getLabelsFailed = (): Notification => ({
...defaultErrorNotification,

View File

@ -1,438 +0,0 @@
import {
labelToRelationship,
labelToIncluded,
taskToTemplate,
variableToTemplate,
dashboardToTemplate,
} from 'src/shared/utils/resourceToTemplate'
import {TemplateType} from '@influxdata/influx'
import {createVariable} from 'src/variables/mocks'
import {
myDashboard,
myView,
myVariable,
myfavelabel,
myfavetask,
myCell,
} from 'src/shared/utils/mocks/resourceToTemplate'
// Libraries
import {RemoteDataState, AppState} from 'src/types'
describe('resourceToTemplate', () => {
const appState = {
resources: {
labels: {
byID: {
[myfavelabel.id]: myfavelabel,
allIDs: [myfavelabel.id],
status: RemoteDataState.Done,
},
},
},
}
describe('labelToRelationship', () => {
it('converts a label to a relationship struct', () => {
const actual = labelToRelationship(myfavelabel)
const expected = {type: TemplateType.Label, id: myfavelabel.id}
expect(actual).toEqual(expected)
})
})
describe('labelToIncluded', () => {
it('converts a label to a data structure in included', () => {
const actual = labelToIncluded(myfavelabel)
const expected = {
type: TemplateType.Label,
id: myfavelabel.id,
attributes: {
name: myfavelabel.name,
properties: {
color: myfavelabel.properties.color,
description: myfavelabel.properties.description,
},
},
}
expect(actual).toEqual(expected)
})
})
describe('variableToTemplate', () => {
it('converts a variable with dependencies to a template', () => {
const a = {
...createVariable('a', 'x.b + 1'),
labels: [myfavelabel.id],
}
const b = createVariable('b', '9000')
const dependencies: any = [a, b]
const actual = variableToTemplate(
(appState as unknown) as AppState,
myVariable,
dependencies
)
const expected = {
meta: {
version: '1',
name: 'beep-Template',
type: 'variable',
description: 'template created from variable: beep',
},
content: {
data: {
type: 'variable',
id: '039ae3b3b74b0000',
attributes: {
name: 'beep',
arguments: {
type: 'query',
values: {
query: 'f(x: v.a)',
language: 'flux',
},
},
selected: null,
},
relationships: {
variable: {
data: [
{
id: 'a',
type: 'variable',
},
{
id: 'b',
type: 'variable',
},
],
},
label: {
data: [],
},
},
},
included: [
{
type: 'variable',
id: 'a',
attributes: {
name: 'a',
arguments: {
type: 'query',
values: {
query: 'x.b + 1',
language: 'flux',
},
},
selected: [],
},
relationships: {
label: {
data: [
{
type: 'label',
id: 'myfavelabel1',
},
],
},
},
},
{
type: 'variable',
id: 'b',
attributes: {
name: 'b',
arguments: {
type: 'query',
values: {
query: '9000',
language: 'flux',
},
},
selected: [],
},
relationships: {
label: {
data: [],
},
},
},
{
id: 'myfavelabel1',
type: 'label',
attributes: {
name: '1label',
properties: {color: 'fffff', description: 'omg'},
},
},
],
},
labels: [],
}
expect(actual).toEqual(expected)
})
})
describe('taskToTemplate', () => {
it('converts a task to a template', () => {
const label = {
id: '037b0c86a92a2000',
name: 'yum',
properties: {
color: '#FF8564',
description: '',
},
}
const state = {
resources: {
labels: {
byID: {
[label.id]: label,
allIDs: [label.id],
status: RemoteDataState.Done,
},
},
},
}
const actual = taskToTemplate((state as unknown) as AppState, {
...myfavetask,
labels: [label.id],
})
const expected = {
content: {
data: {
type: 'task',
attributes: {
every: '24h0m0s',
flux:
'option task = {name: "lala", every: 24h0m0s, offset: 1m0s}\n\nfrom(bucket: "defnuck")\n\t|> range(start: -task.every)',
name: 'lala',
offset: '1m0s',
status: 'active',
},
relationships: {
label: {
data: [
{
id: '037b0c86a92a2000',
type: 'label',
},
],
},
},
},
included: [
{
attributes: {
name: 'yum',
properties: {
color: '#FF8564',
description: '',
},
},
id: '037b0c86a92a2000',
type: TemplateType.Label,
},
],
},
labels: [],
meta: {
description: 'template created from task: lala',
name: 'lala-Template',
type: 'task',
version: '1',
},
}
expect(actual).toEqual(expected)
})
})
describe('dashboardToTemplate', () => {
it('can convert a dashboard to template', () => {
const myLabeledVar = {
...createVariable('var_1', 'labeled var!'),
labels: [myfavelabel.id],
}
const dashboardWithDupeLabel = {
...myDashboard,
labels: [myfavelabel.id],
}
const actual = dashboardToTemplate(
(appState as unknown) as AppState,
dashboardWithDupeLabel,
[myCell],
[myView],
[myLabeledVar]
)
const expected = {
meta: {
version: '1',
name: 'MyDashboard-Template',
type: 'dashboard',
description: 'template created from dashboard: MyDashboard',
},
content: {
data: {
type: 'dashboard',
attributes: {
name: 'MyDashboard',
description: '',
},
relationships: {
label: {
data: [
{
id: 'myfavelabel1',
type: 'label',
},
],
},
cell: {
data: [
{
type: 'cell',
id: 'cell_view_1',
},
],
},
variable: {
data: [
{
type: 'variable',
id: 'var_1',
},
],
},
},
},
included: [
{
id: 'myfavelabel1',
type: 'label',
attributes: {
name: '1label',
properties: {color: 'fffff', description: 'omg'},
},
},
{
id: 'cell_view_1',
type: 'cell',
attributes: {
x: 0,
y: 0,
w: 4,
h: 4,
},
relationships: {
view: {
data: {
type: 'view',
id: 'cell_view_1',
},
},
},
},
{
type: 'view',
id: 'cell_view_1',
attributes: {
name: 'My Cell',
properties: {
shape: 'chronograf-v2',
queries: [
{
text: 'v.bucket',
editMode: 'advanced',
name: 'View Query',
builderConfig: {
buckets: [],
tags: [
{
key: '_measurement',
values: [],
aggregateFunctionType: 'filter',
},
],
functions: [{name: 'mean'}],
aggregateWindow: {period: 'auto', fillValues: false},
},
},
],
axes: {
x: {
bounds: ['', ''],
label: '',
prefix: '',
suffix: '',
base: '10',
scale: 'linear',
},
y: {
bounds: ['', ''],
label: '',
prefix: '',
suffix: '',
base: '10',
scale: 'linear',
},
},
type: 'xy',
legend: {},
geom: 'line',
colors: [],
note: '',
showNoteWhenEmpty: false,
xColumn: null,
yColumn: null,
position: 'overlaid',
},
},
},
{
type: 'variable',
id: 'var_1',
attributes: {
name: 'var_1',
arguments: {
type: 'query',
values: {
query: 'labeled var!',
language: 'flux',
},
},
selected: [],
},
relationships: {
label: {
data: [
{
type: 'label',
id: 'myfavelabel1',
},
],
},
},
},
],
},
labels: [],
}
expect(actual).toEqual(expected)
})
})
})

View File

@ -1,350 +0,0 @@
import {get, pick, flatMap, uniqBy} from 'lodash'
import {defaultBuilderConfig} from 'src/views/helpers'
import {getLabels} from 'src/resources/selectors'
import {
AppState,
Task,
Label,
Dashboard,
DashboardQuery,
Cell,
View,
Variable,
LabelRelationship,
LabelIncluded,
} from 'src/types'
import {TemplateType, DocumentCreate, ITemplate} from '@influxdata/influx'
const CURRENT_TEMPLATE_VERSION = '1'
const blankTemplate = () => ({
meta: {version: CURRENT_TEMPLATE_VERSION},
content: {data: {}, included: []},
labels: [],
})
const blankTaskTemplate = () => {
const baseTemplate = blankTemplate()
return {
...baseTemplate,
meta: {...baseTemplate.meta, type: TemplateType.Task},
content: {
...baseTemplate.content,
data: {...baseTemplate.content.data, type: TemplateType.Task},
},
}
}
const blankVariableTemplate = () => {
const baseTemplate = blankTemplate()
return {
...baseTemplate,
meta: {...baseTemplate.meta, type: TemplateType.Variable},
content: {
...baseTemplate.content,
data: {...baseTemplate.content.data, type: TemplateType.Variable},
},
}
}
const blankDashboardTemplate = () => {
const baseTemplate = blankTemplate()
return {
...baseTemplate,
meta: {...baseTemplate.meta, type: TemplateType.Dashboard},
content: {
...baseTemplate.content,
data: {...baseTemplate.content.data, type: TemplateType.Dashboard},
},
}
}
export const labelToRelationship = (l: Label): LabelRelationship => {
return {type: TemplateType.Label, id: l.id}
}
export const labelToIncluded = (l: Label): LabelIncluded => {
return {
type: TemplateType.Label,
id: l.id,
attributes: {
name: l.name,
properties: l.properties,
},
}
}
export const taskToTemplate = (
state: AppState,
task: Task,
baseTemplate = blankTaskTemplate()
): DocumentCreate => {
const taskName = get(task, 'name', '')
const templateName = `${taskName}-Template`
const taskAttributes = pick(task, [
'status',
'name',
'flux',
'every',
'cron',
'offset',
])
const taskLabels = getLabels(state, task.labels)
const includedLabels = taskLabels.map(label => labelToIncluded(label))
const relationshipsLabels = taskLabels.map(label =>
labelToRelationship(label)
)
const template = {
...baseTemplate,
meta: {
...baseTemplate.meta,
name: templateName,
description: `template created from task: ${taskName}`,
},
content: {
...baseTemplate.content,
data: {
...baseTemplate.content.data,
type: TemplateType.Task,
attributes: taskAttributes,
relationships: {
[TemplateType.Label]: {data: relationshipsLabels},
},
},
included: [...baseTemplate.content.included, ...includedLabels],
},
}
return template
}
const viewToIncluded = (view: View) => {
let properties = view.properties
if ('queries' in properties) {
const sanitizedQueries = properties.queries.map((q: DashboardQuery) => {
return {
...q,
editMode: 'advanced' as 'advanced',
builderConfig: defaultBuilderConfig(),
}
})
properties = {
...properties,
queries: sanitizedQueries,
}
}
return {
type: TemplateType.View,
id: view.id,
attributes: {name: view.name, properties},
}
}
const viewToRelationship = (view: View) => ({
type: TemplateType.View,
id: view.id,
})
const cellToIncluded = (cell: Cell, views: View[]) => {
const cellView = views.find(v => v.id === cell.id)
const viewRelationship = viewToRelationship(cellView)
const cellAttributes = pick(cell, ['x', 'y', 'w', 'h'])
return {
id: cell.id,
type: TemplateType.Cell,
attributes: cellAttributes,
relationships: {
[TemplateType.View]: {
data: viewRelationship,
},
},
}
}
const cellToRelationship = (cell: Cell) => ({
type: TemplateType.Cell,
id: cell.id,
})
export const variableToTemplate = (
state: AppState,
v: Variable,
dependencies: Variable[],
baseTemplate = blankVariableTemplate()
) => {
const labelsByID = state.resources.labels.byID
const variableName = get(v, 'name', '')
const templateName = `${variableName}-Template`
const variableData = variableToIncluded(v, labelsByID)
const variableRelationships = dependencies.map(d => variableToRelationship(d))
const includedDependencies = dependencies.map(d =>
variableToIncluded(d, labelsByID)
)
const vLabels = getLabels(state, v.labels)
const includedLabels = vLabels.map(label => labelToIncluded(label))
const labelRelationships = vLabels.map(label => labelToRelationship(label))
const includedDependentLabels = flatMap(dependencies, d => {
const dLabels = getLabels(state, d.labels)
return dLabels.map(label => labelToIncluded(label))
})
return {
...baseTemplate,
meta: {
...baseTemplate.meta,
name: templateName,
description: `template created from variable: ${variableName}`,
},
content: {
...baseTemplate.content,
data: {
...baseTemplate.content.data,
...variableData,
relationships: {
[TemplateType.Variable]: {
data: [...variableRelationships],
},
[TemplateType.Label]: {
data: [...labelRelationships],
},
},
},
included: [
...includedDependencies,
...includedLabels,
...includedDependentLabels,
],
},
}
}
type LabelsByID = AppState['resources']['labels']['byID']
const variableToIncluded = (v: Variable, labelsByID: LabelsByID) => {
const variableAttributes = pick(v, ['name', 'arguments', 'selected'])
const labelRelationships = v.labels
.map(labelID => {
const label = labelsByID[labelID]
if (!label) {
return null
}
return labelToRelationship(label)
})
.filter(label => !!label)
return {
id: v.id,
type: TemplateType.Variable,
attributes: variableAttributes,
relationships: {
[TemplateType.Label]: {
data: [...labelRelationships],
},
},
}
}
const variableToRelationship = (v: Variable) => ({
type: TemplateType.Variable,
id: v.id,
})
export const dashboardToTemplate = (
state: AppState,
dashboard: Dashboard,
cells: Cell[],
views: View[],
variables: Variable[],
baseTemplate = blankDashboardTemplate()
): DocumentCreate => {
const labelsByID = state.resources.labels.byID
const dashboardName = get(dashboard, 'name', '')
const templateName = `${dashboardName}-Template`
const dashboardAttributes = pick(dashboard, ['name', 'description'])
const dashboardLabels = getLabels(state, dashboard.labels)
const dashboardIncludedLabels = dashboardLabels.map(label =>
labelToIncluded(label)
)
const relationshipsLabels = dashboardLabels.map(label =>
labelToRelationship(label)
)
const includedCells = cells.map(c => cellToIncluded(c, views))
const relationshipsCells = cells.map(c => cellToRelationship(c))
const includedVariables = variables.map(v =>
variableToIncluded(v, labelsByID)
)
const variableIncludedLabels = flatMap(variables, v =>
getLabels(state, v.labels).map(label => labelToIncluded(label))
)
const relationshipsVariables = variables.map(v => variableToRelationship(v))
const includedViews = views.map(v => viewToIncluded(v))
const includedLabels = uniqBy(
[...dashboardIncludedLabels, ...variableIncludedLabels],
'id'
)
const template = {
...baseTemplate,
meta: {
...baseTemplate.meta,
name: templateName,
description: `template created from dashboard: ${dashboardName}`,
},
content: {
...baseTemplate.content,
data: {
...baseTemplate.content.data,
type: TemplateType.Dashboard,
attributes: dashboardAttributes,
relationships: {
[TemplateType.Label]: {data: relationshipsLabels},
[TemplateType.Cell]: {data: relationshipsCells},
[TemplateType.Variable]: {data: relationshipsVariables},
},
},
included: [
...baseTemplate.content.included,
...includedLabels,
...includedCells,
...includedViews,
...includedVariables,
],
},
}
return template
}
export const templateToExport = (template: ITemplate): DocumentCreate => {
const pickedTemplate = pick(template, ['meta', 'content'])
const labelsArray = template.labels.map(l => l.name)
const templateWithLabels = {...pickedTemplate, labels: labelsArray}
return templateWithLabels
}
export const addOrgIDToTemplate = (
template: DocumentCreate,
orgID: string
): DocumentCreate => {
return {...template, orgID}
}

View File

@ -23,7 +23,6 @@
@import 'src/onboarding/OnboardingWizard.scss';
@import 'src/shared/components/protoboard_icon/ProtoboardIcon.scss';
@import 'src/shared/components/columns_options/ColumnsOptions.scss';
@import 'src/shared/components/ImportOverlay.scss';
@import 'src/shared/components/VersionInfo.scss';
@import 'src/shared/components/WaitingText.scss';
@import 'src/shared/components/cells/react-grid-layout.scss';
@ -38,7 +37,6 @@
@import 'src/shared/components/inlineLabels/InlineLabelsEditor.scss';
@import 'src/shared/components/TagInput.scss';
@import 'src/shared/components/ColorSchemeDropdownItem.scss';
@import 'src/shared/components/ExportOverlay.scss';
@import 'src/shared/components/EditableName.scss';
@import 'src/shared/components/SingleStat.scss';
@import 'src/shared/components/DragAndDrop.scss';
@ -98,7 +96,6 @@
@import 'src/shared/components/Checkbox.scss';
@import 'src/shared/components/dapperScrollbars/DapperScrollbars.scss';
@import 'src/shared/components/search_widget/SearchWidget.scss';
@import 'src/templates/components/createFromTemplateOverlay/CreateFromTemplateOverlay.scss';
@import 'src/onboarding/components/SigninForm.scss';
@import 'src/onboarding/containers/LoginPage.scss';
@import 'src/shared/components/ThresholdsSettings.scss';

View File

@ -5,13 +5,11 @@ import {normalize} from 'normalizr'
// APIs
import * as api from 'src/client'
import {createTaskFromTemplate as createTaskFromTemplateAJAX} from 'src/templates/api'
// Schemas
import {taskSchema, arrayOfTasks} from 'src/schemas/tasks'
// Actions
import {setExportTemplate} from 'src/templates/actions/creators'
import {notify, Action as NotifyAction} from 'src/shared/actions/notifications'
import {
addTask,
@ -34,7 +32,6 @@ import * as copy from 'src/shared/copy/notifications'
// Types
import {
Label,
TaskTemplate,
Task,
GetState,
TaskSchedule,
@ -46,7 +43,6 @@ import {
// Utils
import {getErrorMessage} from 'src/utils/api'
import {insertPreambleInScript} from 'src/shared/utils/insertPreambleInScript'
import {taskToTemplate} from 'src/shared/utils/resourceToTemplate'
import {isLimitError} from 'src/cloud/utils/limits'
import {checkTaskLimits} from 'src/cloud/actions/limits'
import {getOrg} from 'src/organizations/selectors'
@ -432,52 +428,6 @@ export const getLogs = (taskID: string, runID: string) => async (
}
}
export const convertToTemplate = (taskID: string) => async (
dispatch,
getState: GetState
): Promise<void> => {
try {
dispatch(setExportTemplate(RemoteDataState.Loading))
const resp = await api.getTask({taskID})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
const {entities, result} = normalize<Task, TaskEntities, string>(
resp.data,
taskSchema
)
const taskTemplate = taskToTemplate(getState(), entities.tasks[result])
dispatch(setExportTemplate(RemoteDataState.Done, taskTemplate))
} catch (error) {
dispatch(setExportTemplate(RemoteDataState.Error))
dispatch(notify(copy.createTemplateFailed(error)))
}
}
export const createTaskFromTemplate = (template: TaskTemplate) => async (
dispatch: Dispatch<Action>,
getState: GetState
): Promise<void> => {
try {
const org = getOrg(getState())
await createTaskFromTemplateAJAX(template, org.id)
dispatch(getTasks())
dispatch(notify(copy.importTaskSucceeded()))
dispatch(checkTaskLimits())
} catch (error) {
if (isLimitError(error)) {
dispatch(notify(copy.resourceLimitReached('tasks')))
} else {
dispatch(notify(copy.importTaskFailed(error)))
}
}
}
export const runDuration = (finishedAt: Date, startedAt: Date): string => {
let timeTag = 'seconds'

View File

@ -3,7 +3,7 @@ import React, {PureComponent} from 'react'
// Components
import {EmptyState} from '@influxdata/clockface'
import AddResourceDropdown from 'src/shared/components/AddResourceDropdown'
import AddResourceButton from 'src/shared/components/AddResourceButton'
// Types
import {ComponentSize} from '@influxdata/clockface'
@ -12,19 +12,11 @@ interface Props {
searchTerm: string
onCreate: () => void
totalCount: number
onImportTask: () => void
onImportFromTemplate: () => void
}
export default class EmptyTasksLists extends PureComponent<Props> {
public render() {
const {
searchTerm,
onCreate,
totalCount,
onImportTask,
onImportFromTemplate,
} = this.props
const {searchTerm, onCreate, totalCount} = this.props
if (totalCount && searchTerm === '') {
return (
@ -40,13 +32,7 @@ export default class EmptyTasksLists extends PureComponent<Props> {
<EmptyState.Text>
Looks like you don't have any <b>Tasks</b>, why not create one?"
</EmptyState.Text>
<AddResourceDropdown
canImportFromTemplate
onSelectNew={onCreate}
onSelectImport={onImportTask}
onSelectTemplate={onImportFromTemplate}
resourceName="Task"
/>
<AddResourceButton onSelectNew={onCreate} resourceName="Task" />
</EmptyState>
)
}

View File

@ -106,7 +106,6 @@ export class TaskCard extends PureComponent<
return (
<Context>
<Context.Menu icon={IconFont.CogThick}>
<Context.Item label="Export" action={this.handleExport} />
<Context.Item label="View Task Runs" action={this.handleViewRuns} />
<Context.Item label="Run Task" action={onRunTask} value={task.id} />
</Context.Menu>
@ -168,15 +167,6 @@ export class TaskCard extends PureComponent<
onUpdate(name, id)
}
private handleExport = () => {
const {
history,
task,
location: {pathname},
} = this.props
history.push(`${pathname}/${task.id}/export`)
}
private get labels(): JSX.Element {
const {task, onFilterChange} = this.props

View File

@ -1,63 +0,0 @@
import React, {PureComponent} from 'react'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import {connect, ConnectedProps} from 'react-redux'
// Components
import ExportOverlay from 'src/shared/components/ExportOverlay'
// Actions
import {convertToTemplate as convertToTemplateAction} from 'src/tasks/actions/thunks'
import {clearExportTemplate as clearExportTemplateAction} from 'src/templates/actions/thunks'
// Types
import {AppState} from 'src/types'
type ReduxProps = ConnectedProps<typeof connector>
type Props = ReduxProps & RouteComponentProps<{orgID: string; id: string}>
class TaskExportOverlay extends PureComponent<Props> {
public componentDidMount() {
const {
match: {
params: {id},
},
convertToTemplate,
} = this.props
convertToTemplate(id)
}
public render() {
const {taskTemplate, status} = this.props
return (
<ExportOverlay
resourceName="Task"
resource={taskTemplate}
onDismissOverlay={this.onDismiss}
status={status}
/>
)
}
private onDismiss = () => {
const {history, clearExportTemplate} = this.props
history.goBack()
clearExportTemplate()
}
}
const mstp = (state: AppState) => ({
taskTemplate: state.resources.templates.exportTemplate.item,
status: state.resources.templates.exportTemplate.status,
})
const mdtp = {
convertToTemplate: convertToTemplateAction,
clearExportTemplate: clearExportTemplateAction,
}
const connector = connect(mstp, mdtp)
export default connector(withRouter(TaskExportOverlay))

View File

@ -1,162 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import {connect, ConnectedProps} from 'react-redux'
import {sortBy} from 'lodash'
// Components
import {
Button,
ComponentColor,
ComponentStatus,
Overlay,
} from '@influxdata/clockface'
import TemplateBrowser from 'src/templates/components/createFromTemplateOverlay/TemplateBrowser'
import TemplateBrowserEmpty from 'src/tasks/components/TemplateBrowserEmpty'
import GetResources from 'src/resources/components/GetResources'
// Actions
import {createTaskFromTemplate as createTaskFromTemplateAction} from 'src/tasks/actions/thunks'
import {getTemplateByID} from 'src/templates/actions/thunks'
// Types
import {
TemplateSummary,
Template,
TemplateType,
AppState,
TaskTemplate,
ResourceType,
} from 'src/types'
// Selectors
import {getAll} from 'src/resources/selectors/getAll'
interface State {
selectedTemplateSummary: TemplateSummary
selectedTemplate: Template
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = ReduxProps
class TaskImportFromTemplateOverlay extends PureComponent<
Props & RouteComponentProps<{orgID: string}>,
State
> {
constructor(props) {
super(props)
this.state = {
selectedTemplateSummary: null,
selectedTemplate: null,
}
}
render() {
return (
<Overlay visible={true} testID="task-import-template--overlay">
<GetResources resources={[ResourceType.Templates]}>
<Overlay.Container maxWidth={900}>
<Overlay.Header
title="Create Task from a Template"
onDismiss={this.onDismiss}
/>
<Overlay.Body>{this.overlayBody}</Overlay.Body>
<Overlay.Footer>
<Button
text="Cancel"
onClick={this.onDismiss}
key="cancel-button"
/>
<Button
text="Create Task"
onClick={this.onSubmit}
key="submit-button"
testID="create-task-button"
color={ComponentColor.Success}
status={this.submitStatus}
/>
</Overlay.Footer>
</Overlay.Container>
</GetResources>
</Overlay>
)
}
private get overlayBody(): JSX.Element {
const {selectedTemplateSummary, selectedTemplate} = this.state
const {templates} = this.props
if (!templates.length) {
return <TemplateBrowserEmpty />
}
return (
<TemplateBrowser
templates={templates}
selectedTemplate={selectedTemplate}
selectedTemplateSummary={selectedTemplateSummary}
onSelectTemplate={this.handleSelectTemplate}
/>
)
}
private get submitStatus(): ComponentStatus {
const {selectedTemplate} = this.state
return selectedTemplate ? ComponentStatus.Default : ComponentStatus.Disabled
}
private handleSelectTemplate = async (
selectedTemplateSummary: TemplateSummary
): Promise<void> => {
const selectedTemplate = await getTemplateByID(selectedTemplateSummary.id)
this.setState({
selectedTemplateSummary,
selectedTemplate,
})
}
private onDismiss = () => {
const {history, match} = this.props
history.push(`/orgs/${match.params.orgID}/tasks`)
}
private onSubmit = () => {
const {createTaskFromTemplate} = this.props
const taskTemplate = this.state.selectedTemplate as TaskTemplate
createTaskFromTemplate(taskTemplate)
this.onDismiss()
}
}
const mstp = (state: AppState) => {
const {
resources: {
templates: {status},
},
} = state
const items = getAll(state, ResourceType.Templates)
const filteredTemplates = items.filter(
t => !t.meta.type || t.meta.type === TemplateType.Task
)
const templates = sortBy(filteredTemplates, item =>
item.meta.name.toLocaleLowerCase()
)
return {
templates,
templateStatus: status,
}
}
const mdtp = {
createTaskFromTemplate: createTaskFromTemplateAction,
}
const connector = connect(mstp, mdtp)
export default connector(withRouter(TaskImportFromTemplateOverlay))

View File

@ -1,79 +0,0 @@
import React, {PureComponent} from 'react'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import {connect, ConnectedProps} from 'react-redux'
// Components
import ImportOverlay from 'src/shared/components/ImportOverlay'
// Copy
import {invalidJSON} from 'src/shared/copy/notifications'
// Actions
import {createTaskFromTemplate as createTaskFromTemplateAction} from 'src/tasks/actions/thunks'
import {notify as notifyAction} from 'src/shared/actions/notifications'
// Types
import {ComponentStatus} from '@influxdata/clockface'
// Utils
import jsonlint from 'jsonlint-mod'
interface State {
status: ComponentStatus
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = ReduxProps & RouteComponentProps<{orgID: string}>
class TaskImportOverlay extends PureComponent<Props> {
public state: State = {
status: ComponentStatus.Default,
}
public render() {
return (
<ImportOverlay
onDismissOverlay={this.onDismiss}
resourceName="Task"
onSubmit={this.handleImportTask}
status={this.state.status}
updateStatus={this.updateOverlayStatus}
/>
)
}
private onDismiss = () => {
const {history} = this.props
history.goBack()
}
private updateOverlayStatus = (status: ComponentStatus) =>
this.setState(() => ({status}))
private handleImportTask = (importString: string) => {
const {createTaskFromTemplate, notify} = this.props
let template
this.updateOverlayStatus(ComponentStatus.Default)
try {
template = jsonlint.parse(importString)
} catch (error) {
this.updateOverlayStatus(ComponentStatus.Error)
notify(invalidJSON(error.message))
return
}
createTaskFromTemplate(template)
this.onDismiss()
}
}
const mdtp = {
createTaskFromTemplate: createTaskFromTemplateAction,
notify: notifyAction,
}
const connector = connect(null, mdtp)
export default connector(withRouter(TaskImportOverlay))

View File

@ -11,7 +11,7 @@ import {
FlexBox,
FlexDirection,
} from '@influxdata/clockface'
import AddResourceDropdown from 'src/shared/components/AddResourceDropdown'
import AddResourceButton from 'src/shared/components/AddResourceButton'
import SearchWidget from 'src/shared/components/search_widget/SearchWidget'
import ResourceSortDropdown from 'src/shared/components/resource_sort_dropdown/ResourceSortDropdown'
import RateLimitAlert from 'src/cloud/components/RateLimitAlert'
@ -27,9 +27,7 @@ interface Props {
onCreateTask: () => void
setShowInactive: () => void
showInactive: boolean
onImportTask: () => void
limitStatus: LimitStatus
onImportFromTemplate: () => void
searchTerm: string
setSearchTerm: typeof setSearchTermAction
sortKey: TaskSortKey
@ -48,8 +46,6 @@ export default class TasksHeader extends PureComponent<Props> {
onCreateTask,
setShowInactive,
showInactive,
onImportTask,
onImportFromTemplate,
setSearchTerm,
searchTerm,
sortKey,
@ -92,11 +88,8 @@ export default class TasksHeader extends PureComponent<Props> {
onChange={setShowInactive}
/>
</FlexBox>
<AddResourceDropdown
canImportFromTemplate
<AddResourceButton
onSelectNew={onCreateTask}
onSelectImport={onImportTask}
onSelectTemplate={onImportFromTemplate}
resourceName="Task"
limitStatus={limitStatus}
/>

View File

@ -29,12 +29,10 @@ interface Props {
onRunTask: any
onUpdate: (name: string, taskID: string) => void
filterComponent?: JSX.Element
onImportTask: () => void
sortKey: TaskSortKey
sortDirection: Sort
sortType: SortTypes
checkTaskLimits: any
onImportFromTemplate: () => void
}
interface State {
@ -60,13 +58,7 @@ export default class TasksList extends PureComponent<Props, State> {
}
public render() {
const {
searchTerm,
onCreate,
totalCount,
onImportTask,
onImportFromTemplate,
} = this.props
const {searchTerm, onCreate, totalCount} = this.props
return (
<>
@ -77,8 +69,6 @@ export default class TasksList extends PureComponent<Props, State> {
searchTerm={searchTerm}
onCreate={onCreate}
totalCount={totalCount}
onImportTask={onImportTask}
onImportFromTemplate={onImportFromTemplate}
/>
}
>

View File

@ -1,60 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import {connect} from 'react-redux'
// Components
import {
EmptyState,
ComponentSize,
Button,
IconFont,
} from '@influxdata/clockface'
// Types
import {AppState, Organization} from 'src/types'
// Selectors
import {getOrg} from 'src/organizations/selectors'
interface StateProps {
org: Organization
}
type Props = StateProps & RouteComponentProps<{orgID: string}>
class TemplateBrowserEmpty extends PureComponent<Props> {
public render() {
return (
<div className="import-template-overlay--empty">
<EmptyState size={ComponentSize.Large}>
<EmptyState.Text>
Looks like you don't have any <b>Templates</b> yet, why not import
one?
</EmptyState.Text>
<Button
size={ComponentSize.Medium}
text="Import One Here"
icon={IconFont.CogThick}
onClick={this.handleButtonClick}
/>
</EmptyState>
</div>
)
}
private handleButtonClick = (): void => {
const {history, org} = this.props
history.push(`/orgs/${org.id}/settings/templates/import`)
}
}
const mstp = (state: AppState) => ({
org: getOrg(state),
})
export default connect<StateProps, {}>(
mstp,
null
)(withRouter(TemplateBrowserEmpty))

View File

@ -1,7 +1,6 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect, ConnectedProps} from 'react-redux'
import {Switch, Route} from 'react-router-dom'
// Components
import TasksHeader from 'src/tasks/components/TasksHeader'
@ -12,9 +11,6 @@ import FilterList from 'src/shared/components/FilterList'
import GetResources from 'src/resources/components/GetResources'
import GetAssetLimits from 'src/cloud/components/GetAssetLimits'
import AssetLimitAlert from 'src/cloud/components/AssetLimitAlert'
import TaskExportOverlay from 'src/tasks/components/TaskExportOverlay'
import TaskImportOverlay from 'src/tasks/components/TaskImportOverlay'
import TaskImportFromTemplateOverlay from 'src/tasks/components/TaskImportFromTemplateOverlay'
// Utils
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
@ -100,8 +96,6 @@ class TasksPage extends PureComponent<Props, State> {
onCreateTask={this.handleCreateTask}
setShowInactive={setShowInactive}
showInactive={showInactive}
onImportTask={this.summonImportOverlay}
onImportFromTemplate={this.summonImportFromTemplateOverlay}
limitStatus={limitStatus}
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
@ -131,10 +125,6 @@ class TasksPage extends PureComponent<Props, State> {
onRunTask={onRunTask}
onFilterChange={setSearchTerm}
onUpdate={updateTaskName}
onImportTask={this.summonImportOverlay}
onImportFromTemplate={
this.summonImportFromTemplateOverlay
}
sortKey={sortKey}
sortDirection={sortDirection}
sortType={sortType}
@ -151,20 +141,6 @@ class TasksPage extends PureComponent<Props, State> {
</GetResources>
</Page.Contents>
</Page>
<Switch>
<Route
path="/orgs/:orgID/tasks/:id/export"
component={TaskExportOverlay}
/>
<Route
path="/orgs/:orgID/tasks/import-template"
component={TaskImportFromTemplateOverlay}
/>
<Route
path="/orgs/:orgID/tasks/import"
component={TaskImportOverlay}
/>
</Switch>
</>
)
}
@ -200,28 +176,6 @@ class TasksPage extends PureComponent<Props, State> {
history.push(`/orgs/${orgID}/tasks/new`)
}
private summonImportFromTemplateOverlay = () => {
const {
history,
match: {
params: {orgID},
},
} = this.props
history.push(`/orgs/${orgID}/tasks/import-template`)
}
private summonImportOverlay = (): void => {
const {
history,
match: {
params: {orgID},
},
} = this.props
history.push(`/orgs/${orgID}/tasks/import`)
}
private get filteredTasks(): Task[] {
const {tasks, showInactive} = this.props
const matchingTasks = tasks.filter(t => {

View File

@ -1,23 +1,9 @@
// Types
import {
CommunityTemplate,
RemoteDataState,
TemplateSummaryEntities,
} from 'src/types'
import {DocumentCreate} from '@influxdata/influx'
import {NormalizedSchema} from 'normalizr'
import {CommunityTemplate} from 'src/types'
import {InstalledStack} from 'src/types'
export const ADD_TEMPLATE_SUMMARY = 'ADD_TEMPLATE_SUMMARY'
export const GET_TEMPLATE_SUMMARIES_FOR_ORG = 'GET_TEMPLATE_SUMMARIES_FOR_ORG'
export const POPULATE_TEMPLATE_SUMMARIES = 'POPULATE_TEMPLATE_SUMMARIES'
export const REMOVE_TEMPLATE_SUMMARY = 'REMOVE_TEMPLATE_SUMMARY'
export const SET_STAGED_TEMPLATE = 'SET_STAGED_TEMPLATE'
export const SET_STAGED_TEMPLATE_URL = 'SET_STAGED_TEMPLATE_URL'
export const SET_EXPORT_TEMPLATE = 'SET_EXPORT_TEMPLATE'
export const SET_TEMPLATE_SUMMARY = 'SET_TEMPLATE_SUMMARY'
export const SET_TEMPLATES_STATUS = 'SET_TEMPLATES_STATUS'
export const TOGGLE_TEMPLATE_RESOURCE_INSTALL =
'TOGGLE_TEMPLATE_RESOURCE_INSTALL'
@ -25,73 +11,13 @@ export const SET_STACKS = 'SET_STACKS'
export const DELETE_STACKS = 'DELETE_STACKS'
export type Action =
| ReturnType<typeof addTemplateSummary>
| ReturnType<typeof populateTemplateSummaries>
| ReturnType<typeof removeTemplateSummary>
| ReturnType<typeof setExportTemplate>
| ReturnType<typeof setTemplatesStatus>
| ReturnType<typeof setTemplateSummary>
| ReturnType<typeof setStagedCommunityTemplate>
| ReturnType<typeof setStagedTemplateUrl>
| ReturnType<typeof toggleTemplateResourceInstall>
| ReturnType<typeof setStacks>
| ReturnType<typeof removeStack>
type TemplateSummarySchema<R extends string | string[]> = NormalizedSchema<
TemplateSummaryEntities,
R
>
// Action Creators
export const addTemplateSummary = (schema: TemplateSummarySchema<string>) =>
({
type: ADD_TEMPLATE_SUMMARY,
schema,
} as const)
export const populateTemplateSummaries = (
schema: TemplateSummarySchema<string[]>
) =>
({
type: POPULATE_TEMPLATE_SUMMARIES,
status: RemoteDataState.Done,
schema,
} as const)
export const setExportTemplate = (
status: RemoteDataState,
item?: DocumentCreate
) =>
({
type: SET_EXPORT_TEMPLATE,
status,
item,
} as const)
export const setTemplatesStatus = (status: RemoteDataState) =>
({
type: SET_TEMPLATES_STATUS,
status,
} as const)
export const removeTemplateSummary = (id: string) =>
({
type: REMOVE_TEMPLATE_SUMMARY,
id,
} as const)
export const setTemplateSummary = (
id: string,
status: RemoteDataState,
schema?: TemplateSummarySchema<string>
) =>
({
type: SET_TEMPLATE_SUMMARY,
id,
status,
schema,
} as const)
export const setStagedCommunityTemplate = (template: CommunityTemplate) =>
({
type: SET_STAGED_TEMPLATE,

View File

@ -1,283 +1,16 @@
// Libraries
import {normalize} from 'normalizr'
// APIs
import {client} from 'src/utils/api'
import {fetchStacks} from 'src/templates/api'
import {createDashboardFromTemplate} from 'src/dashboards/actions/thunks'
import {createVariableFromTemplate} from 'src/variables/actions/thunks'
import {createTaskFromTemplate} from 'src/tasks/actions/thunks'
// Schemas
import {templateSchema, arrayOfTemplates} from 'src/schemas/templates'
// Actions
import {notify, Action as NotifyAction} from 'src/shared/actions/notifications'
import {
addTemplateSummary,
setStacks,
populateTemplateSummaries,
removeTemplateSummary,
setExportTemplate,
setTemplatesStatus,
setTemplateSummary,
Action as TemplateAction,
} from 'src/templates/actions/creators'
// Constants
import * as copy from 'src/shared/copy/notifications'
import {staticTemplates} from 'src/templates/constants/defaultTemplates'
// Types
import {Dispatch} from 'react'
import {DocumentCreate, TemplateType} from '@influxdata/influx'
import {
RemoteDataState,
GetState,
DashboardTemplate,
VariableTemplate,
TemplateSummary,
TemplateSummaryEntities,
Label,
Template,
TaskTemplate,
ResourceType,
} from 'src/types'
// Utils
import {templateToExport} from 'src/shared/utils/resourceToTemplate'
import {getOrg} from 'src/organizations/selectors'
import {getLabels, getStatus} from 'src/resources/selectors'
type Action = TemplateAction | NotifyAction
export const getTemplateByID = async (id: string): Promise<Template> => {
const template: Template = (await client.templates.get(id)) as any
return template
}
export const getTemplates = () => async (
dispatch: Dispatch<Action>,
getState: GetState
): Promise<void> => {
const state = getState()
if (getStatus(state, ResourceType.Templates) === RemoteDataState.NotStarted) {
dispatch(setTemplatesStatus(RemoteDataState.Loading))
}
const org = getOrg(state)
const items = await client.templates.getAll(org.id)
const templateSummaries = normalize<
TemplateSummary,
TemplateSummaryEntities,
string[]
>(items, arrayOfTemplates)
dispatch(populateTemplateSummaries(templateSummaries))
}
export const createTemplate = (template: DocumentCreate) => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
try {
const org = getOrg(getState())
const item = await client.templates.create({...template, orgID: org.id})
const templateSummary = normalize<
TemplateSummary,
TemplateSummaryEntities,
string
>(item, templateSchema)
dispatch(addTemplateSummary(templateSummary))
dispatch(notify(copy.importTemplateSucceeded()))
} catch (error) {
console.error(error)
dispatch(notify(copy.importTemplateFailed(error)))
}
}
export const createTemplateFromResource = (
resource: DocumentCreate,
resourceName: string
) => async (dispatch: Dispatch<Action>, getState: GetState) => {
try {
const org = getOrg(getState())
await client.templates.create({...resource, orgID: org.id})
dispatch(notify(copy.resourceSavedAsTemplate(resourceName)))
} catch (error) {
console.error(error)
dispatch(notify(copy.saveResourceAsTemplateFailed(resourceName, error)))
}
}
export const updateTemplate = (id: string, props: TemplateSummary) => async (
dispatch: Dispatch<Action>,
getState: GetState
): Promise<void> => {
setTemplateSummary(id, RemoteDataState.Loading)
const state = getState()
const labels = getLabels(state, props.labels)
try {
const item = await client.templates.update(id, {...props, labels})
const templateSummary = normalize<
TemplateSummary,
TemplateSummaryEntities,
string
>(item, templateSchema)
dispatch(setTemplateSummary(id, RemoteDataState.Done, templateSummary))
dispatch(notify(copy.updateTemplateSucceeded()))
} catch (error) {
console.error(error)
dispatch(notify(copy.updateTemplateFailed(error)))
}
}
export const convertToTemplate = (id: string) => async (
dispatch: Dispatch<Action>
): Promise<void> => {
try {
dispatch(setExportTemplate(RemoteDataState.Loading))
const templateDocument = await client.templates.get(id)
const template = templateToExport(templateDocument)
dispatch(setExportTemplate(RemoteDataState.Done, template))
} catch (error) {
dispatch(setExportTemplate(RemoteDataState.Error))
dispatch(notify(copy.createTemplateFailed(error)))
}
}
export const clearExportTemplate = () => (dispatch: Dispatch<Action>) => {
dispatch(setExportTemplate(RemoteDataState.NotStarted, null))
}
export const deleteTemplate = (templateID: string) => async (
dispatch: Dispatch<Action>
): Promise<void> => {
try {
await client.templates.delete(templateID)
dispatch(removeTemplateSummary(templateID))
dispatch(notify(copy.deleteTemplateSuccess()))
} catch (e) {
console.error(e)
dispatch(notify(copy.deleteTemplateFailed(e)))
}
}
export const cloneTemplate = (templateID: string) => async (
dispatch: Dispatch<Action>,
getState: GetState
): Promise<void> => {
try {
const org = getOrg(getState())
const createdTemplate = await client.templates.clone(templateID, org.id)
const templateSummary = normalize<
TemplateSummary,
TemplateSummaryEntities,
string
>(createdTemplate, templateSchema)
dispatch(addTemplateSummary(templateSummary))
dispatch(notify(copy.cloneTemplateSuccess()))
} catch (error) {
console.error(error)
dispatch(notify(copy.cloneTemplateFailed(error)))
}
}
const createFromTemplate = (template: Template) => dispatch => {
const {
content: {
data: {type},
},
} = template
try {
switch (type) {
case TemplateType.Dashboard:
return dispatch(
createDashboardFromTemplate(template as DashboardTemplate)
)
case TemplateType.Task:
return dispatch(createTaskFromTemplate(template as TaskTemplate))
case TemplateType.Variable:
return dispatch(
createVariableFromTemplate(template as VariableTemplate)
)
default:
throw new Error(`Cannot create template: ${type}`)
}
} catch (e) {
console.error(e)
dispatch(notify(copy.createResourceFromTemplateFailed(e)))
}
}
export const createResourceFromStaticTemplate = (name: string) => dispatch => {
const template = staticTemplates[name]
dispatch(createFromTemplate(template))
}
export const createResourceFromTemplate = (templateID: string) => async (
dispatch
): Promise<void> => {
const template: Template = (await client.templates.get(templateID)) as any
dispatch(createFromTemplate(template))
}
export const addTemplateLabelsAsync = (
templateID: string,
labels: Label[]
) => async (dispatch: Dispatch<Action>): Promise<void> => {
try {
await client.templates.addLabels(
templateID,
labels.map(l => l.id)
)
const item = await client.templates.get(templateID)
const templateSummary = normalize<
TemplateSummary,
TemplateSummaryEntities,
string
>(item, templateSchema)
dispatch(
setTemplateSummary(templateID, RemoteDataState.Done, templateSummary)
)
} catch (error) {
console.error(error)
dispatch(notify(copy.addTemplateLabelFailed()))
}
}
export const removeTemplateLabelsAsync = (
templateID: string,
labels: Label[]
) => async (dispatch: Dispatch<Action>): Promise<void> => {
try {
await client.templates.removeLabels(
templateID,
labels.map(l => l.id)
)
const item = await client.templates.get(templateID)
const templateSummary = normalize<
TemplateSummary,
TemplateSummaryEntities,
string
>(item, templateSchema)
dispatch(
setTemplateSummary(templateID, RemoteDataState.Done, templateSummary)
)
} catch (error) {
console.error(error)
dispatch(notify(copy.removeTemplateLabelFailed()))
}
}
type Action = TemplateAction
export const fetchAndSetStacks = (orgID: string) => async (
dispatch: Dispatch<Action>

View File

@ -4,7 +4,6 @@ import {normalize} from 'normalizr'
// Schemas
import {arrayOfVariables, variableSchema} from 'src/schemas/variables'
import {taskSchema} from 'src/schemas/tasks'
// Utils
import {
@ -21,12 +20,8 @@ import {addLabelDefaults} from 'src/labels/utils'
// API
import {
getDashboard as apiGetDashboard,
getTask as apiGetTask,
postTask as apiPostTask,
postTasksLabel as apiPostTasksLabel,
getLabels as apiGetLabels,
postLabel as apiPostLabel,
getVariable as apiGetVariable,
getVariables as apiGetVariables,
postVariable as apiPostVariable,
postVariablesLabel as apiPostVariablesLabel,
@ -46,7 +41,6 @@ import {addDashboardDefaults} from 'src/schemas/dashboards'
// Types
import {
TaskEntities,
DashboardTemplate,
Dashboard,
TemplateType,
@ -55,9 +49,7 @@ import {
CellIncluded,
LabelIncluded,
ViewIncluded,
TaskTemplate,
TemplateBase,
Task,
VariableTemplate,
Variable,
VariableEntities,
@ -358,112 +350,6 @@ const createVariablesFromTemplate = async (
await Promise.all(addLabelsToVars)
}
export const createTaskFromTemplate = async (
template: TaskTemplate,
orgID: string
): Promise<Task> => {
const {content} = template
try {
if (
content.data.type !== TemplateType.Task ||
template.meta.version !== '1'
) {
throw new Error('Cannot create task from this template')
}
const flux = content.data.attributes.flux
const postResp = await apiPostTask({data: {orgID, flux}})
if (postResp.status !== 201) {
throw new Error(postResp.data.message)
}
const {entities, result} = normalize<Task, TaskEntities, string>(
postResp.data,
taskSchema
)
const postedTask = entities.tasks[result]
// associate imported label.id with created label
const labelMap = await createLabelsFromTemplate(template, orgID)
await addTaskLabelsFromTemplate(template, labelMap, postedTask)
const resp = await apiGetTask({taskID: postedTask.id})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
return postedTask
} catch (e) {
console.error(e)
}
}
const addTaskLabelsFromTemplate = async (
template: TaskTemplate,
labelMap: LabelMap,
task: Task
) => {
try {
const relationships = getLabelRelationships(template.content.data)
const labelIDs = relationships.map(l => labelMap[l.id] || '')
const pending = labelIDs.map(labelID =>
apiPostTasksLabel({taskID: task.id, data: {labelID}})
)
const resolved = await Promise.all(pending)
if (resolved.length > 0 && resolved.some(r => r.status !== 201)) {
throw new Error('An error occurred adding task labels from the templates')
}
} catch (error) {
console.error(error)
}
}
export const createVariableFromTemplate = async (
template: VariableTemplate,
orgID: string
) => {
const {content} = template
try {
if (
content.data.type !== TemplateType.Variable ||
template.meta.version !== '1'
) {
throw new Error('Cannot create variable from this template')
}
const resp = await apiPostVariable({
data: {
...content.data.attributes,
orgID,
},
})
if (resp.status !== 201) {
throw new Error(resp.data.message)
}
// associate imported label.id with created label
const labelsMap = await createLabelsFromTemplate(template, orgID)
await createVariablesFromTemplate(template, labelsMap, orgID)
const variable = await apiGetVariable({variableID: resp.data.id})
if (variable.status !== 200) {
throw new Error(variable.data.message)
}
return variable.data
} catch (error) {
console.error(error)
}
}
const applyTemplates = async params => {
const resp = await postTemplatesApply(params)
if (resp.status >= 300) {

View File

@ -7,7 +7,7 @@ import {CommunityTemplateOverlay} from 'src/templates/components/CommunityTempla
// Actions
import {setStagedCommunityTemplate} from 'src/templates/actions/creators'
import {createTemplate, fetchAndSetStacks} from 'src/templates/actions/thunks'
import {fetchAndSetStacks} from 'src/templates/actions/thunks'
import {notify} from 'src/shared/actions/notifications'
import {getTotalResourceCount} from 'src/templates/selectors'
@ -151,7 +151,6 @@ const mstp = (state: AppState, props: RouterProps) => {
}
const mdtp = {
createTemplate,
notify,
setStagedCommunityTemplate,
fetchAndSetStacks,

View File

@ -1,47 +0,0 @@
// Libraries
import React, {FunctionComponent} from 'react'
// Components
import {
EmptyState,
IconFont,
ComponentColor,
Button,
} from '@influxdata/clockface'
// Types
import {ComponentSize} from '@influxdata/clockface'
interface Props {
searchTerm: string
onImport: () => void
}
const EmptyTemplatesList: FunctionComponent<Props> = ({
searchTerm,
onImport,
}) => {
if (searchTerm === '') {
return (
<EmptyState size={ComponentSize.Large}>
<EmptyState.Text>
Looks like you don't have any <b>Templates</b>, why not create one?
</EmptyState.Text>
<Button
text="Import Template"
icon={IconFont.Plus}
color={ComponentColor.Primary}
onClick={onImport}
/>
</EmptyState>
)
}
return (
<EmptyState size={ComponentSize.Large}>
<EmptyState.Text>No Templates match your search term</EmptyState.Text>
</EmptyState>
)
}
export default EmptyTemplatesList

View File

@ -1,114 +0,0 @@
// Libraries
import React, {PureComponent, MouseEvent} from 'react'
import {get, capitalize} from 'lodash'
import {connect, ConnectedProps} from 'react-redux'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import {
Button,
ComponentSize,
FlexBox,
FlexDirection,
JustifyContent,
} from '@influxdata/clockface'
// Components
import {ResourceCard} from '@influxdata/clockface'
// Actions
import {createResourceFromStaticTemplate} from 'src/templates/actions/thunks'
// Selectors
import {getOrg} from 'src/organizations/selectors'
// Types
import {ComponentColor} from '@influxdata/clockface'
import {AppState, TemplateSummary} from 'src/types'
// Constants
interface OwnProps {
template: TemplateSummary
name: string
onFilterChange: (searchTerm: string) => void
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = ReduxProps & OwnProps
class StaticTemplateCard extends PureComponent<
Props & RouteComponentProps<{orgID: string}>
> {
public render() {
const {template} = this.props
return (
<ResourceCard testID="template-card" contextMenu={this.contextMenu}>
<ResourceCard.Name
onClick={this.handleNameClick}
name={template.meta.name}
testID="template-card--name"
/>
{this.description}
<ResourceCard.Meta>
{capitalize(get(template, 'content.data.type', ''))}
</ResourceCard.Meta>
</ResourceCard>
)
}
private get contextMenu(): JSX.Element {
return (
<FlexBox
margin={ComponentSize.Medium}
direction={FlexDirection.Row}
justifyContent={JustifyContent.FlexEnd}
>
<Button
text="Create"
color={ComponentColor.Primary}
size={ComponentSize.ExtraSmall}
onClick={this.handleCreate}
/>
</FlexBox>
)
}
private get description(): JSX.Element {
const {template} = this.props
const description = get(template, 'content.data.attributes.description')
return (
<ResourceCard.Description description={description || 'No description'} />
)
}
private handleCreate = () => {
const {onCreateFromTemplate, name} = this.props
onCreateFromTemplate(name)
}
private handleNameClick = (e: MouseEvent<HTMLAnchorElement>) => {
e.preventDefault()
this.handleViewTemplate()
}
private handleViewTemplate = () => {
const {history, org, name} = this.props
history.push(`/orgs/${org.id}/settings/templates/${name}/static/view`)
}
}
const mstp = (state: AppState) => {
return {
org: getOrg(state),
}
}
const mdtp = {
onCreateFromTemplate: createResourceFromStaticTemplate,
}
const connector = connect(mstp, mdtp)
export default connector(withRouter(StaticTemplateCard))

View File

@ -1,55 +0,0 @@
import React, {PureComponent} from 'react'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import _ from 'lodash'
// Components
import ViewOverlay from 'src/shared/components/ViewOverlay'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Types
import {RemoteDataState} from '@influxdata/clockface'
import {staticTemplates} from 'src/templates/constants/defaultTemplates'
import {DashboardTemplate} from 'src/types'
interface OwnProps {
match: {id: string}
}
type Props = OwnProps & RouteComponentProps<{orgID: string; id: string}>
@ErrorHandling
class TemplateExportOverlay extends PureComponent<Props> {
public render() {
return (
<ViewOverlay
resource={this.template}
overlayHeading={this.overlayTitle}
onDismissOverlay={this.onDismiss}
status={RemoteDataState.Done}
/>
)
}
private get template(): DashboardTemplate {
const {
match: {
params: {id},
},
} = this.props
return staticTemplates[id]
}
private get overlayTitle() {
return this.template.meta.name
}
private onDismiss = () => {
const {history} = this.props
history.goBack()
}
}
export default withRouter(TemplateExportOverlay)

View File

@ -1,81 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import memoizeOne from 'memoize-one'
// Components
import {ResourceList} from '@influxdata/clockface'
import EmptyTemplatesList from 'src/templates/components/EmptyTemplatesList'
import StaticTemplateCard from 'src/templates/components/StaticTemplateCard'
// Types
import {Template, TemplateSummary, RemoteDataState} from 'src/types'
import {SortTypes} from 'src/shared/utils/sort'
import {Sort} from 'src/clockface'
// Selectors
import {getSortedResources} from 'src/shared/utils/sort'
export type TemplateOrSummary = Template | TemplateSummary
export interface StaticTemplate {
name: string
template: TemplateOrSummary
}
interface Props {
templates: StaticTemplate[]
searchTerm: string
onFilterChange: (searchTerm: string) => void
onImport: () => void
sortKey: string
sortDirection: Sort
sortType: SortTypes
}
export default class StaticTemplatesList extends PureComponent<Props> {
private memGetSortedResources = memoizeOne<typeof getSortedResources>(
getSortedResources
)
public render() {
const {searchTerm, onImport} = this.props
return (
<ResourceList>
<ResourceList.Body
emptyState={
<EmptyTemplatesList searchTerm={searchTerm} onImport={onImport} />
}
>
{this.rows}
</ResourceList.Body>
</ResourceList>
)
}
private get rows(): JSX.Element[] {
const {
templates,
sortKey,
sortDirection,
sortType,
onFilterChange,
} = this.props
const sortedTemplates = this.memGetSortedResources(
templates,
`template.${sortKey}`,
sortDirection,
sortType
)
return sortedTemplates.map(t => (
<StaticTemplateCard
key={`template-id--static-${t.name}`}
name={t.name}
template={{...t.template, status: RemoteDataState.Done}}
onFilterChange={onFilterChange}
/>
))
}
}

View File

@ -1,206 +0,0 @@
// Libraries
import React, {PureComponent, MouseEvent} from 'react'
import {connect, ConnectedProps} from 'react-redux'
import {get, capitalize} from 'lodash'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import {
Button,
ComponentSize,
FlexBox,
FlexDirection,
JustifyContent,
} from '@influxdata/clockface'
// Components
import {Context} from 'src/clockface'
import {ResourceCard, IconFont} from '@influxdata/clockface'
import InlineLabels from 'src/shared/components/inlineLabels/InlineLabels'
// Actions
import {
deleteTemplate,
cloneTemplate,
updateTemplate,
createResourceFromTemplate,
removeTemplateLabelsAsync,
addTemplateLabelsAsync,
} from 'src/templates/actions/thunks'
// Selectors
import {getOrg} from 'src/organizations/selectors'
// Types
import {ComponentColor} from '@influxdata/clockface'
import {AppState, Label, TemplateSummary} from 'src/types'
// Constants
import {DEFAULT_TEMPLATE_NAME} from 'src/templates/constants'
interface OwnProps {
template: TemplateSummary
onFilterChange: (searchTerm: string) => void
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = ReduxProps & OwnProps
class TemplateCard extends PureComponent<
Props & RouteComponentProps<{orgID: string}>
> {
public render() {
const {template, onFilterChange} = this.props
return (
<ResourceCard testID="template-card" contextMenu={this.contextMenu}>
<ResourceCard.EditableName
onClick={this.handleNameClick}
onUpdate={this.handleUpdateTemplateName}
name={template.meta.name}
noNameString={DEFAULT_TEMPLATE_NAME}
testID="template-card--name"
buttonTestID="template-card--name-button"
inputTestID="template-card--input"
/>
{this.description}
<ResourceCard.Meta>
{capitalize(get(template, 'content.data.type', ''))}
</ResourceCard.Meta>
<InlineLabels
selectedLabelIDs={template.labels}
onFilterChange={onFilterChange}
onAddLabel={this.handleAddLabel}
onRemoveLabel={this.handleRemoveLabel}
/>
</ResourceCard>
)
}
private handleUpdateTemplateName = (name: string) => {
const {template} = this.props
this.props.onUpdate(template.id, {
...template,
meta: {...template.meta, name},
})
}
private handleUpdateTemplateDescription = (description: string) => {
const {template} = this.props
this.props.onUpdate(template.id, {
...template,
meta: {...template.meta, description},
})
}
private get description(): JSX.Element {
const {template} = this.props
const description = get(template, 'meta.description', '')
const name = get(template, 'meta.name', '')
return (
<ResourceCard.EditableDescription
onUpdate={this.handleUpdateTemplateDescription}
description={description}
placeholder={`Describe ${name} Template`}
/>
)
}
private get contextMenu(): JSX.Element {
const {
template: {id},
onDelete,
} = this.props
return (
<FlexBox
margin={ComponentSize.Medium}
direction={FlexDirection.Row}
justifyContent={JustifyContent.FlexEnd}
>
<Button
text="Create"
color={ComponentColor.Primary}
size={ComponentSize.ExtraSmall}
onClick={this.handleCreate}
/>
<Context>
<Context.Menu
icon={IconFont.Duplicate}
color={ComponentColor.Secondary}
>
<Context.Item label="Clone" action={this.handleClone} value={id} />
</Context.Menu>
<Context.Menu
icon={IconFont.Trash}
color={ComponentColor.Danger}
testID="context-delete-menu"
>
<Context.Item
label="Delete"
action={onDelete}
value={id}
testID="context-delete-task"
/>
</Context.Menu>
</Context>
</FlexBox>
)
}
private handleCreate = () => {
const {onCreateFromTemplate, template} = this.props
onCreateFromTemplate(template.id)
}
private handleClone = () => {
const {
template: {id},
onClone,
} = this.props
onClone(id)
}
private handleNameClick = (e: MouseEvent): void => {
e.preventDefault()
this.handleViewTemplate()
}
private handleViewTemplate = () => {
const {history, template, org} = this.props
history.push(`/orgs/${org.id}/settings/templates/${template.id}/view`)
}
private handleAddLabel = (label: Label): void => {
const {template, onAddTemplateLabels} = this.props
onAddTemplateLabels(template.id, [label])
}
private handleRemoveLabel = (label: Label): void => {
const {template, onRemoveTemplateLabels} = this.props
onRemoveTemplateLabels(template.id, [label])
}
}
const mstp = (state: AppState) => {
return {
org: getOrg(state),
}
}
const mdtp = {
onDelete: deleteTemplate,
onClone: cloneTemplate,
onUpdate: updateTemplate,
onCreateFromTemplate: createResourceFromTemplate,
onAddTemplateLabels: addTemplateLabelsAsync,
onRemoveTemplateLabels: removeTemplateLabelsAsync,
}
const connector = connect(mstp, mdtp)
export default connector(withRouter(TemplateCard))

View File

@ -1,70 +0,0 @@
import React, {PureComponent} from 'react'
import {connect, ConnectedProps} from 'react-redux'
import {withRouter, RouteComponentProps} from 'react-router-dom'
// Components
import ExportOverlay from 'src/shared/components/ExportOverlay'
// Actions
import {
convertToTemplate as convertToTemplateAction,
clearExportTemplate as clearExportTemplateAction,
} from 'src/templates/actions/thunks'
// Types
import {AppState} from 'src/types'
interface OwnProps {
match: {id: string}
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = OwnProps &
ReduxProps &
RouteComponentProps<{orgID: string; id: string}>
class TemplateExportOverlay extends PureComponent<Props> {
public componentDidMount() {
const {
match: {
params: {id},
},
convertToTemplate,
} = this.props
convertToTemplate(id)
}
public render() {
const {exportTemplate, status} = this.props
return (
<ExportOverlay
resourceName="Template"
resource={exportTemplate}
onDismissOverlay={this.onDismiss}
status={status}
/>
)
}
private onDismiss = () => {
const {history, clearExportTemplate} = this.props
history.goBack()
clearExportTemplate()
}
}
const mstp = (state: AppState) => ({
exportTemplate: state.resources.templates.exportTemplate.item,
status: state.resources.templates.exportTemplate.status,
})
const mdtp = {
convertToTemplate: convertToTemplateAction,
clearExportTemplate: clearExportTemplateAction,
}
const connector = connect(mstp, mdtp)
export default connector(withRouter(TemplateExportOverlay))

View File

@ -1,91 +0,0 @@
import React, {PureComponent} from 'react'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import {connect, ConnectedProps} from 'react-redux'
// Components
import ImportOverlay from 'src/shared/components/ImportOverlay'
// Copy
import {invalidJSON} from 'src/shared/copy/notifications'
// Actions
import {createTemplate as createTemplateAction} from 'src/templates/actions/thunks'
import {notify as notifyAction} from 'src/shared/actions/notifications'
// Types
import {AppState, ResourceType, Organization} from 'src/types'
import {ComponentStatus} from '@influxdata/clockface'
// Utils
import jsonlint from 'jsonlint-mod'
import {getByID} from 'src/resources/selectors'
interface State {
status: ComponentStatus
}
type ReduxProps = ConnectedProps<typeof connector>
type RouterProps = RouteComponentProps<{orgID: string}>
type Props = ReduxProps & RouterProps
class TemplateImportOverlay extends PureComponent<Props> {
public state: State = {
status: ComponentStatus.Default,
}
public render() {
return (
<ImportOverlay
onDismissOverlay={this.onDismiss}
resourceName="Template"
onSubmit={this.handleImportTemplate}
status={this.state.status}
updateStatus={this.updateOverlayStatus}
/>
)
}
private onDismiss = () => {
const {history} = this.props
history.goBack()
}
private updateOverlayStatus = (status: ComponentStatus) =>
this.setState(() => ({status}))
private handleImportTemplate = (importString: string) => {
const {createTemplate, notify} = this.props
let template
this.updateOverlayStatus(ComponentStatus.Default)
try {
template = jsonlint.parse(importString)
} catch (error) {
this.updateOverlayStatus(ComponentStatus.Error)
notify(invalidJSON(error.message))
return
}
createTemplate(template)
this.onDismiss()
}
}
const mstp = (state: AppState, props: RouterProps) => {
const org = getByID<Organization>(
state,
ResourceType.Orgs,
props.match.params.orgID
)
return {org}
}
const mdtp = {
notify: notifyAction,
createTemplate: createTemplateAction,
}
const connector = connect(mstp, mdtp)
export default connector(withRouter(TemplateImportOverlay))

View File

@ -1,81 +0,0 @@
import React, {PureComponent} from 'react'
import {connect, ConnectedProps} from 'react-redux'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import _ from 'lodash'
// Components
import ViewOverlay from 'src/shared/components/ViewOverlay'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Actions
import {
convertToTemplate as convertToTemplateAction,
clearExportTemplate as clearExportTemplateAction,
} from 'src/templates/actions/thunks'
// Types
import {AppState} from 'src/types'
interface OwnProps {
match: {id: string}
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = OwnProps &
ReduxProps &
RouteComponentProps<{orgID: string; id: string}>
@ErrorHandling
class TemplateExportOverlay extends PureComponent<Props> {
public componentDidMount() {
const {
match: {
params: {id},
},
convertToTemplate,
} = this.props
convertToTemplate(id)
}
public render() {
const {exportTemplate, status} = this.props
return (
<ViewOverlay
resource={exportTemplate}
overlayHeading={this.overlayTitle}
onDismissOverlay={this.onDismiss}
status={status}
/>
)
}
private get overlayTitle() {
const {exportTemplate} = this.props
if (exportTemplate) {
return exportTemplate.meta.name
}
return ''
}
private onDismiss = () => {
const {history, clearExportTemplate} = this.props
history.goBack()
clearExportTemplate()
}
}
const mstp = (state: AppState) => ({
exportTemplate: state.resources.templates.exportTemplate.item,
status: state.resources.templates.exportTemplate.status,
})
const mdtp = {
convertToTemplate: convertToTemplateAction,
clearExportTemplate: clearExportTemplateAction,
}
const connector = connect(mstp, mdtp)
export default connector(withRouter(TemplateExportOverlay))

View File

@ -1,76 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
import memoizeOne from 'memoize-one'
// Components
import {ResourceList} from '@influxdata/clockface'
import EmptyTemplatesList from 'src/templates/components/EmptyTemplatesList'
import TemplateCard from 'src/templates/components/TemplateCard'
// Types
import {TemplateSummary} from 'src/types'
import {SortTypes} from 'src/shared/utils/sort'
import {Sort} from 'src/clockface'
// Selectors
import {getSortedResources} from 'src/shared/utils/sort'
interface Props {
templates: TemplateSummary[]
searchTerm: string
onFilterChange: (searchTerm: string) => void
onImport: () => void
sortKey: string
sortDirection: Sort
sortType: SortTypes
}
export default class TemplatesList extends PureComponent<Props> {
private memGetSortedResources = memoizeOne<typeof getSortedResources>(
getSortedResources
)
public render() {
const {searchTerm, onImport} = this.props
return (
<>
<ResourceList>
<ResourceList.Body
emptyState={
<EmptyTemplatesList searchTerm={searchTerm} onImport={onImport} />
}
>
{this.rows}
</ResourceList.Body>
</ResourceList>
</>
)
}
private get rows(): JSX.Element[] {
const {
templates,
sortKey,
sortDirection,
sortType,
onFilterChange,
} = this.props
const sortedTemplates = this.memGetSortedResources(
templates,
sortKey,
sortDirection,
sortType
)
return sortedTemplates.map(t => (
<TemplateCard
key={`template-id--${t.id}`}
template={t}
onFilterChange={onFilterChange}
/>
))
}
}

View File

@ -1,219 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {connect, ConnectedProps} from 'react-redux'
// Components
import FilterList from 'src/shared/components/FilterList'
import TemplatesList from 'src/templates/components/TemplatesList'
import StaticTemplatesList, {
StaticTemplate,
TemplateOrSummary,
} from 'src/templates/components/StaticTemplatesList'
import {ErrorHandling} from 'src/shared/decorators/errors'
import SearchWidget from 'src/shared/components/search_widget/SearchWidget'
import GetResources from 'src/resources/components/GetResources'
import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader'
import ResourceSortDropdown from 'src/shared/components/resource_sort_dropdown/ResourceSortDropdown'
// Types
import {AppState, ResourceType, TemplateSummary} from 'src/types'
import {SortTypes} from 'src/shared/utils/sort'
import {
Sort,
Button,
ComponentColor,
IconFont,
SelectGroup,
} from '@influxdata/clockface'
import {TemplateSortKey} from 'src/shared/components/resource_sort_dropdown/generateSortItems'
import {staticTemplates as statics} from 'src/templates/constants/defaultTemplates'
// Selectors
import {getAll} from 'src/resources/selectors/getAll'
// Constants
const staticTemplates: StaticTemplate[] = _.map(statics, (template, name) => ({
name,
template: template as TemplateOrSummary,
}))
interface OwnProps {
onImport: () => void
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = OwnProps & ReduxProps
interface State {
searchTerm: string
sortKey: TemplateSortKey
sortDirection: Sort
sortType: SortTypes
activeTab: string
}
const FilterStaticTemplates = FilterList<StaticTemplate>()
const FilterTemplateSummaries = FilterList<TemplateSummary>()
@ErrorHandling
class TemplatesPage extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
searchTerm: '',
sortKey: 'meta.name',
sortDirection: Sort.Ascending,
sortType: SortTypes.String,
activeTab: 'static-templates',
}
}
public render() {
const {onImport} = this.props
const {activeTab, sortType, sortKey, sortDirection} = this.state
const leftHeaderItems = (
<>
{this.filterComponent}
<SelectGroup>
<SelectGroup.Option
name="template-type"
id="static-templates"
active={activeTab === 'static-templates'}
value="static-templates"
onClick={this.handleClickTab}
titleText="Static Templates"
>
Static Templates
</SelectGroup.Option>
<SelectGroup.Option
name="template-type"
id="user-templates"
active={activeTab === 'user-templates'}
value="user-templates"
onClick={this.handleClickTab}
titleText="User Templates"
>
User Templates
</SelectGroup.Option>
</SelectGroup>
<ResourceSortDropdown
resourceType={ResourceType.Templates}
sortType={sortType}
sortKey={sortKey}
sortDirection={sortDirection}
onSelect={this.handleSort}
/>
</>
)
return (
<>
<TabbedPageHeader
childrenLeft={leftHeaderItems}
childrenRight={
<Button
text="Import Template"
icon={IconFont.Plus}
color={ComponentColor.Primary}
onClick={onImport}
/>
}
/>
{this.templatesList}
</>
)
}
private handleClickTab = val => {
this.setState({activeTab: val})
}
private handleSort = (
sortKey: TemplateSortKey,
sortDirection: Sort,
sortType: SortTypes
): void => {
this.setState({sortKey, sortDirection, sortType})
}
private get templatesList(): JSX.Element {
const {templates, onImport} = this.props
const {searchTerm, sortKey, sortDirection, sortType, activeTab} = this.state
if (activeTab === 'static-templates') {
return (
<FilterStaticTemplates
searchTerm={searchTerm}
searchKeys={['template.meta.name', 'labels[].name']}
list={staticTemplates}
>
{ts => {
return (
<StaticTemplatesList
searchTerm={searchTerm}
templates={ts}
onFilterChange={this.setSearchTerm}
onImport={onImport}
sortKey={sortKey}
sortDirection={sortDirection}
sortType={sortType}
/>
)
}}
</FilterStaticTemplates>
)
}
if (activeTab === 'user-templates') {
return (
<GetResources resources={[ResourceType.Labels]}>
<FilterTemplateSummaries
searchTerm={searchTerm}
searchKeys={['meta.name', 'labels[].name']}
list={templates}
>
{ts => {
return (
<TemplatesList
searchTerm={searchTerm}
templates={ts}
onFilterChange={this.setSearchTerm}
onImport={onImport}
sortKey={sortKey}
sortDirection={sortDirection}
sortType={sortType}
/>
)
}}
</FilterTemplateSummaries>
</GetResources>
)
}
}
private get filterComponent(): JSX.Element {
const {searchTerm} = this.state
return (
<SearchWidget
placeholderText="Filter templates..."
onSearch={this.setSearchTerm}
searchTerm={searchTerm}
/>
)
}
private setSearchTerm = (searchTerm: string) => {
this.setState({searchTerm})
}
}
const mstp = (state: AppState) => ({
templates: getAll(state, ResourceType.Templates),
})
const connector = connect(mstp)
export default connector(TemplatesPage)

View File

@ -1,91 +0,0 @@
.import-template-overlay,
.import-template-overlay--empty {
height: 500px;
display: flex;
}
.import-template-overlay {
align-items: stretch;
}
.import-template-overlay--templates {
flex: 2 0 0;
border-radius: $ix-radius;
}
.import-template-overlay--details {
flex: 5 0 0;
margin-left: $ix-marg-b;
}
.import-template-overlay--panel {
min-height: 500px;
}
.import-template-overlay--name {
margin-top: 0;
margin-bottom: $ix-marg-b;
}
.import-template-overlay--description {
margin-top: 0;
margin-bottom: $ix-marg-d;
}
.import-template-overlay--heading {
margin-top: 0;
border-bottom: $ix-border solid $g5-pepper;
padding-bottom: $ix-marg-b;
}
.import-templates-overlay--included {
margin-bottom: 0;
}
.import-template-overlay--name.missing,
.import-template-overlay--included.missing,
.import-template-overlay--description.missing {
font-style: italic;
color: $g9-mountain;
}
.import-template-overlay--empty {
background-color: $g3-castle;
border-radius: $ix-radius;
align-content: center;
align-items: center;
justify-content: center;
}
.import-template-overlay--template {
user-select: none;
border-radius: $ix-radius;
padding: $ix-marg-b;
background-color: $g1-raven;
margin-bottom: $ix-border;
border: $ix-border solid $g1-raven;
color: $g11-sidewalk;
display: flex;
flex-wrap: none;
align-items: center;
transition: color 0.25s ease, background-color 0.25s ease, border-color 0.25s ease;
&:hover {
border-color: $g5-pepper;
background-color: $g5-pepper;
color: $g18-cloud;
cursor: pointer;
}
&.active {
border-color: $c-rainforest;
color: $g18-cloud;
background-color: $g5-pepper;
}
}
.import-template-overlay--list-label {
font-size: 13px;
font-weight: 500;
margin-left: $ix-marg-b;
}

View File

@ -1,210 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import {connect, ConnectedProps} from 'react-redux'
import {sortBy} from 'lodash'
// Components
import {
Button,
ComponentColor,
ComponentStatus,
Overlay,
} from '@influxdata/clockface'
import TemplateBrowser from 'src/templates/components/createFromTemplateOverlay/TemplateBrowser'
import TemplateBrowserEmpty from 'src/templates/components/createFromTemplateOverlay/TemplateBrowserEmpty'
import GetResources from 'src/resources/components/GetResources'
// Actions
import {createDashboardFromTemplate as createDashboardFromTemplateAction} from 'src/dashboards/actions/thunks'
import {getTemplateByID} from 'src/templates/actions/thunks'
// Constants
import {influxdbTemplateList} from 'src/templates/constants/defaultTemplates'
// Types
import {
TemplateSummary,
Template,
TemplateType,
DashboardTemplateIncluded,
AppState,
DashboardTemplate,
ResourceType,
} from 'src/types'
// Selectors
import {getAll} from 'src/resources/selectors/getAll'
interface State {
selectedTemplateSummary: TemplateSummary
selectedTemplate: Template
variables: string[]
cells: string[]
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = ReduxProps
class DashboardImportFromTemplateOverlay extends PureComponent<
Props & RouteComponentProps<{orgID: string}>,
State
> {
constructor(props) {
super(props)
this.state = {
selectedTemplateSummary: null,
selectedTemplate: null,
variables: [],
cells: [],
}
}
render() {
return (
<GetResources resources={[ResourceType.Templates]}>
<Overlay visible={true}>
<Overlay.Container maxWidth={900}>
<Overlay.Header
title="Create Dashboard from a Template"
onDismiss={this.onDismiss}
/>
<Overlay.Body>{this.overlayBody}</Overlay.Body>
<Overlay.Footer>
<Button
text="Cancel"
onClick={this.onDismiss}
key="cancel-button"
/>
<Button
text="Create Dashboard"
onClick={this.onSubmit}
key="submit-button"
testID="create-dashboard-button"
color={ComponentColor.Success}
status={this.submitStatus}
/>
</Overlay.Footer>
</Overlay.Container>
</Overlay>
</GetResources>
)
}
private get overlayBody(): JSX.Element {
const {
selectedTemplateSummary,
cells,
variables,
selectedTemplate,
} = this.state
const {templates} = this.props
if (!templates.length) {
return <TemplateBrowserEmpty />
}
return (
<TemplateBrowser
templates={templates}
cells={cells}
variables={variables}
selectedTemplate={selectedTemplate}
selectedTemplateSummary={selectedTemplateSummary}
onSelectTemplate={this.handleSelectTemplate}
/>
)
}
private get submitStatus(): ComponentStatus {
const {selectedTemplate} = this.state
return selectedTemplate ? ComponentStatus.Default : ComponentStatus.Disabled
}
private getVariablesForTemplate(template: Template): string[] {
const variables = []
const included = template.content.included as DashboardTemplateIncluded[]
included.forEach(data => {
if (data.type === TemplateType.Variable) {
variables.push(data.attributes.name)
}
})
return variables
}
private getCellsForTemplate(template: Template): string[] {
const cells = []
const included = template.content.included as DashboardTemplateIncluded[]
included.forEach(data => {
if (data.type === TemplateType.View) {
cells.push(data.attributes.name)
}
})
return cells
}
private handleSelectTemplate = async (
selectedTemplateSummary: TemplateSummary
): Promise<void> => {
const {id} = selectedTemplateSummary
let selectedTemplate
if (!id.includes('influxdb-template')) {
selectedTemplate = await getTemplateByID(id)
} else {
selectedTemplate = selectedTemplateSummary
}
this.setState({
selectedTemplateSummary,
selectedTemplate,
variables: this.getVariablesForTemplate(selectedTemplate),
cells: this.getCellsForTemplate(selectedTemplate),
})
}
private onDismiss = () => {
const {history} = this.props
history.goBack()
}
private onSubmit = () => {
const {createDashboardFromTemplate} = this.props
const dashboardTemplate = this.state.selectedTemplate as DashboardTemplate
createDashboardFromTemplate(dashboardTemplate)
this.onDismiss()
}
}
const mstp = (state: AppState) => {
const {
resources: {
templates: {status},
},
} = state
const items = getAll(state, ResourceType.Templates)
const filteredTemplates = items.filter(
t => !t.meta.type || t.meta.type === TemplateType.Dashboard
)
const templates = sortBy(filteredTemplates, item =>
item.meta.name.toLocaleLowerCase()
)
return {
templates: [...templates, ...(influxdbTemplateList as any)],
templateStatus: status,
}
}
const mdtp = {
createDashboardFromTemplate: createDashboardFromTemplateAction,
}
const connector = connect(mstp, mdtp)
export default connector(withRouter(DashboardImportFromTemplateOverlay))

View File

@ -1,48 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import {TemplateSummary, Template} from 'src/types'
import TemplateBrowserDetails from 'src/templates/components/createFromTemplateOverlay/TemplateBrowserDetails'
import TemplateBrowserList from 'src/templates/components/createFromTemplateOverlay/TemplateBrowserList'
interface Props {
templates: TemplateSummary[]
selectedTemplateSummary: TemplateSummary
selectedTemplate: Template
variables?: string[]
cells?: string[]
onSelectTemplate: (selectedTemplateSummary: TemplateSummary) => void
}
class TemplateBrowser extends PureComponent<Props> {
public render() {
const {
selectedTemplateSummary,
cells,
variables,
selectedTemplate,
templates,
onSelectTemplate,
} = this.props
return (
<div className="import-template-overlay">
<TemplateBrowserList
templates={templates}
onSelectTemplate={onSelectTemplate}
selectedTemplateSummary={selectedTemplateSummary}
/>
<TemplateBrowserDetails
cells={cells}
variables={variables}
selectedTemplateSummary={selectedTemplateSummary}
selectedTemplate={selectedTemplate}
/>
</div>
)
}
}
export default TemplateBrowser

View File

@ -1,146 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import {
Panel,
EmptyState,
ComponentSize,
Grid,
Columns,
DapperScrollbars,
} from '@influxdata/clockface'
import {TemplateSummary, Template} from 'src/types'
interface Props {
selectedTemplateSummary: TemplateSummary
selectedTemplate: Template
variables: string[]
cells: string[]
}
class TemplateBrowserDetails extends PureComponent<Props> {
public render() {
return (
<DapperScrollbars
className="import-template-overlay--details"
autoSize={false}
>
<Panel
testID="template-panel"
className="import-template-overlay--panel"
>
<Panel.Body size={ComponentSize.Medium}>
{this.panelContents}
</Panel.Body>
</Panel>
</DapperScrollbars>
)
}
private get panelContents(): JSX.Element {
const {selectedTemplateSummary} = this.props
if (!selectedTemplateSummary) {
return (
<EmptyState size={ComponentSize.Medium}>
<EmptyState.Text>Select a Template from the left</EmptyState.Text>
</EmptyState>
)
}
return (
<Grid>
<Grid.Row>
<Grid.Column widthSM={Columns.Twelve}>
{this.templateName}
{this.templateDescription}
</Grid.Column>
{this.props.variables && (
<Grid.Column widthSM={Columns.Six}>
<h5 className="import-template-overlay--heading">Variables</h5>
{this.variablesList}
</Grid.Column>
)}
{this.props.cells && (
<Grid.Column widthSM={Columns.Six}>
<h5 className="import-template-overlay--heading">Cells</h5>
{this.cellsList}
</Grid.Column>
)}
</Grid.Row>
</Grid>
)
}
private get variablesList(): JSX.Element | JSX.Element[] {
const {variables} = this.props
if (!variables.length) {
return (
<p className="import-template-overlay--included missing">
No included variables
</p>
)
}
return variables.map((variable, i) => (
<p
className="import-templates-overlay--included"
key={`${i} ${variable}`}
>
{variable}
</p>
))
}
private get cellsList(): JSX.Element | JSX.Element[] {
const {cells} = this.props
if (!cells.length) {
return (
<p className="import-template-overlay--included missing">
No included cells
</p>
)
}
return cells.map((cell, i) => (
<p className="import-templates-overlay--included" key={`${i} ${cell}`}>
{cell}
</p>
))
}
private get templateDescription(): JSX.Element {
const {selectedTemplateSummary} = this.props
const description = _.get(selectedTemplateSummary, 'meta.description')
if (description) {
return (
<p className="import-template-overlay--description">{description}</p>
)
}
return (
<p className="import-template-overlay--description missing">
No description
</p>
)
}
private get templateName(): JSX.Element {
const {selectedTemplateSummary} = this.props
const name = _.get(selectedTemplateSummary, 'meta.name')
const templateName = name || 'Untitled'
const className = name
? 'import-template-overlay--name'
: 'import-template-overlay--name missing'
return <h3 className={className}>{templateName}</h3>
}
}
export default TemplateBrowserDetails

View File

@ -1,60 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import {connect} from 'react-redux'
// Components
import {
EmptyState,
ComponentSize,
Button,
IconFont,
} from '@influxdata/clockface'
// Types
import {AppState, Organization} from 'src/types'
// Selectors
import {getOrg} from 'src/organizations/selectors'
interface StateProps {
org: Organization
}
type Props = StateProps & RouteComponentProps<{orgID: string}>
class TemplateBrowserEmpty extends PureComponent<Props> {
public render() {
return (
<div className="import-template-overlay--empty">
<EmptyState size={ComponentSize.Large}>
<EmptyState.Text>
Looks like you don't have any <b>Templates</b> yet, why not import
one?
</EmptyState.Text>
<Button
size={ComponentSize.Medium}
text="Go to Templates Settings"
icon={IconFont.CogThick}
onClick={this.handleButtonClick}
/>
</EmptyState>
</div>
)
}
private handleButtonClick = (): void => {
const {history, org} = this.props
history.push(`/orgs/${org.id}/settings/templates`)
}
}
const mstp = (state: AppState) => ({
org: getOrg(state),
})
export default connect<StateProps, {}>(
mstp,
null
)(withRouter(TemplateBrowserEmpty))

View File

@ -1,43 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import {get, orderBy} from 'lodash'
// Components
import {DapperScrollbars} from '@influxdata/clockface'
import {TemplateSummary} from 'src/types'
import TemplateBrowserListItem from 'src/templates/components/createFromTemplateOverlay/TemplateBrowserListItem'
interface Props {
templates: TemplateSummary[]
selectedTemplateSummary: TemplateSummary
onSelectTemplate: (selectedTemplateSummary: TemplateSummary) => void
}
class TemplateBrowser extends PureComponent<Props> {
public render() {
const {selectedTemplateSummary, templates, onSelectTemplate} = this.props
return (
<DapperScrollbars
className="import-template-overlay--templates"
autoSize={false}
noScrollX={true}
>
{orderBy(templates, [({meta: {name}}) => name.toLocaleLowerCase()]).map(
t => (
<TemplateBrowserListItem
key={t.id}
template={t}
label={t.meta.name}
onClick={onSelectTemplate}
testID={`template--${t.meta.name}`}
selected={get(selectedTemplateSummary, 'id', '') === t.id}
/>
)
)}
</DapperScrollbars>
)
}
}
export default TemplateBrowser

View File

@ -1,49 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import classnames from 'classnames'
import {TemplateSummary} from 'src/types'
// Components
import {Icon, IconFont} from '@influxdata/clockface'
interface Props {
onClick: (template: TemplateSummary) => void
template: TemplateSummary
label: string
selected: boolean
testID: string
}
class TemplateBrowser extends PureComponent<Props> {
public render() {
const {testID, label} = this.props
return (
<div
className={this.className}
data-testid={testID}
onClick={this.handleClick}
>
<Icon
glyph={IconFont.Cube}
className="import-template-overlay--list-icon"
/>
<span className="import-template-overlay--list-label">{label}</span>
</div>
)
}
private get className(): string {
const {selected} = this.props
return classnames('import-template-overlay--template', {active: selected})
}
private handleClick = (): void => {
const {onClick, template} = this.props
onClick(template)
}
}
export default TemplateBrowser

View File

@ -13,7 +13,6 @@ import {notify} from 'src/shared/actions/notifications'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {CommunityTemplateImportOverlay} from 'src/templates/components/CommunityTemplateImportOverlay'
import {CommunityTemplatesInstalledList} from 'src/templates/components/CommunityTemplatesInstalledList'
import {
Bullet,
Button,
@ -29,16 +28,13 @@ import {
} from '@influxdata/clockface'
import SettingsTabbedPage from 'src/settings/components/SettingsTabbedPage'
import SettingsHeader from 'src/settings/components/SettingsHeader'
import {communityTemplatesImportPath} from 'src/templates/containers/TemplatesIndex'
import GetResources from 'src/resources/components/GetResources'
import {getOrg} from 'src/organizations/selectors'
import {setStagedTemplateUrl} from 'src/templates/actions/creators'
// Utils
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
import {getOrg} from 'src/organizations/selectors'
import {
getGithubUrlFromTemplateDetails,
getTemplateNameFromUrl,
@ -47,14 +43,15 @@ import {reportError} from 'src/shared/utils/errors'
import {communityTemplateUnsupportedFormatError} from 'src/shared/copy/notifications'
import {event} from 'src/cloud/utils/reporting'
// Types
import {AppState, ResourceType} from 'src/types'
import {event} from 'src/cloud/utils/reporting'
const communityTemplatesUrl =
'https://github.com/influxdata/community-templates#templates'
const templatesPath = '/orgs/:orgID/settings/templates'
const communityTemplatesImportPath = `${templatesPath}/import/:directory/:templateName/:templateExtension`
type Params = {
params: {directory: string; templateName: string; templateExtension: string}

View File

@ -1,88 +0,0 @@
import React, {Component} from 'react'
import {RouteComponentProps} from 'react-router-dom'
import {connect, ConnectedProps} from 'react-redux'
import {Switch, Route} from 'react-router-dom'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Page} from '@influxdata/clockface'
import SettingsTabbedPage from 'src/settings/components/SettingsTabbedPage'
import SettingsHeader from 'src/settings/components/SettingsHeader'
import TemplatesPage from 'src/templates/components/TemplatesPage'
import GetResources from 'src/resources/components/GetResources'
import TemplateImportOverlay from 'src/templates/components/TemplateImportOverlay'
import TemplateExportOverlay from 'src/templates/components/TemplateExportOverlay'
import TemplateViewOverlay from 'src/templates/components/TemplateViewOverlay'
import StaticTemplateViewOverlay from 'src/templates/components/StaticTemplateViewOverlay'
import {CommunityTemplatesIndex} from 'src/templates/containers/CommunityTemplatesIndex'
// Utils
import {pageTitleSuffixer} from 'src/shared/utils/pageTitles'
import {getOrg} from 'src/organizations/selectors'
// Types
import {AppState, ResourceType} from 'src/types'
type ReduxProps = ConnectedProps<typeof connector>
type Props = RouteComponentProps & ReduxProps
const templatesPath = '/orgs/:orgID/settings/templates'
export const communityTemplatesImportPath = `${templatesPath}/import/:directory/:templateName/:templateExtension`
@ErrorHandling
class TemplatesIndex extends Component<Props> {
public render() {
const {org, flags} = this.props
if (flags.communityTemplates) {
return <CommunityTemplatesIndex />
}
return (
<>
<Page titleTag={pageTitleSuffixer(['Templates', 'Settings'])}>
<SettingsHeader />
<SettingsTabbedPage activeTab="templates" orgID={org.id}>
<GetResources resources={[ResourceType.Templates]}>
<TemplatesPage onImport={this.handleImport} />
</GetResources>
</SettingsTabbedPage>
</Page>
<Switch>
<Route
path={`${templatesPath}/import`}
component={TemplateImportOverlay}
/>
<Route
path={`${templatesPath}/:id/export`}
component={TemplateExportOverlay}
/>
<Route
path={`${templatesPath}/:id/view`}
component={TemplateViewOverlay}
/>
<Route
path={`${templatesPath}/:id/static/view`}
component={StaticTemplateViewOverlay}
/>
</Switch>
</>
)
}
private handleImport = () => {
const {history, org} = this.props
history.push(`/orgs/${org.id}/settings/templates/import`)
}
}
const mstp = (state: AppState) => {
return {
org: getOrg(state),
flags: state.flags.original,
}
}
const connector = connect(mstp)
export default connector(TemplatesIndex)

View File

@ -1,131 +0,0 @@
// Libraries
import {normalize} from 'normalizr'
// Schema
import {templateSchema, arrayOfTemplates} from 'src/schemas/templates'
// Reducer
import {templatesReducer as reducer} from 'src/templates/reducers'
// Actions
import {
addTemplateSummary,
populateTemplateSummaries,
removeTemplateSummary,
setTemplateSummary,
} from 'src/templates/actions/creators'
// Types
import {
CommunityTemplate,
RemoteDataState,
TemplateSummaryEntities,
TemplateSummary,
} from 'src/types'
const status = RemoteDataState.Done
const templateSummary = {
links: {
self: '/api/v2/documents/templates/051ff6b3a8d23000',
},
id: '1',
meta: {
name: 'foo',
type: 'dashboard',
description: 'A template dashboard for something',
version: '1',
},
labels: [],
status,
}
const exportTemplate = {status, item: null}
const stagedCommunityTemplate: CommunityTemplate = {}
const initialState = () => ({
stagedCommunityTemplate,
stagedTemplateUrl: '',
status,
byID: {
['1']: templateSummary,
['2']: {...templateSummary, id: '2'},
},
allIDs: [templateSummary.id, '2'],
exportTemplate,
stacks: [],
})
describe('templates reducer', () => {
it('can set the templatess', () => {
const schema = normalize<
TemplateSummary,
TemplateSummaryEntities,
string[]
>([templateSummary], arrayOfTemplates)
const byID = schema.entities.templates
const allIDs = schema.result
const actual = reducer(undefined, populateTemplateSummaries(schema))
expect(actual.byID).toEqual(byID)
expect(actual.allIDs).toEqual(allIDs)
})
it('can add a template', () => {
const id = '3'
const anotherTemplateSummary = {...templateSummary, id}
const schema = normalize<TemplateSummary, TemplateSummaryEntities, string>(
anotherTemplateSummary,
templateSchema
)
const state = initialState()
const actual = reducer(state, addTemplateSummary(schema))
expect(actual.allIDs.length).toEqual(Number(id))
})
it('can remove a template', () => {
const allIDs = [templateSummary.id]
const byID = {[templateSummary.id]: templateSummary}
const state = initialState()
const expected = {
status,
byID,
allIDs,
exportTemplate,
stagedCommunityTemplate,
stagedTemplateUrl: '',
stacks: [],
}
const actual = reducer(state, removeTemplateSummary(state.allIDs[1]))
expect(actual).toEqual(expected)
})
it('can set a template', () => {
const name = 'updated name'
const loadedTemplateSummary = {
...templateSummary,
meta: {...templateSummary.meta, name: 'updated name'},
}
const schema = normalize<TemplateSummary, TemplateSummaryEntities, string>(
loadedTemplateSummary,
templateSchema
)
const state = initialState()
const actual = reducer(
state,
setTemplateSummary(templateSummary.id, RemoteDataState.Done, schema)
)
expect(actual.byID[templateSummary.id].meta.name).toEqual(name)
})
})

View File

@ -1,30 +1,12 @@
import {produce} from 'immer'
import {
Action,
ADD_TEMPLATE_SUMMARY,
POPULATE_TEMPLATE_SUMMARIES,
REMOVE_TEMPLATE_SUMMARY,
SET_EXPORT_TEMPLATE,
SET_STACKS,
SET_STAGED_TEMPLATE,
SET_STAGED_TEMPLATE_URL,
SET_TEMPLATES_STATUS,
SET_TEMPLATE_SUMMARY,
TOGGLE_TEMPLATE_RESOURCE_INSTALL,
} from 'src/templates/actions/creators'
import {
CommunityTemplate,
ResourceType,
RemoteDataState,
TemplateSummary,
TemplatesState,
} from 'src/types'
import {
addResource,
removeResource,
setResource,
setResourceAtID,
} from 'src/resources/reducers/helpers'
import {CommunityTemplate, RemoteDataState, TemplatesState} from 'src/types'
const defaultCommunityTemplate = (): CommunityTemplate => {
return {
@ -55,28 +37,6 @@ export const templatesReducer = (
): TemplatesState =>
produce(state, draftState => {
switch (action.type) {
case POPULATE_TEMPLATE_SUMMARIES: {
setResource<TemplateSummary>(draftState, action, ResourceType.Templates)
return
}
case SET_TEMPLATES_STATUS: {
const {status} = action
draftState.status = status
return
}
case SET_TEMPLATE_SUMMARY: {
setResourceAtID<TemplateSummary>(
draftState,
action,
ResourceType.Templates
)
return
}
case SET_STAGED_TEMPLATE_URL: {
const {templateUrl} = action
@ -177,30 +137,6 @@ export const templatesReducer = (
return
}
case SET_EXPORT_TEMPLATE: {
const {status, item} = action
draftState.exportTemplate.status = status
if (item) {
draftState.exportTemplate.item = item
} else {
draftState.exportTemplate.item = null
}
return
}
case REMOVE_TEMPLATE_SUMMARY: {
removeResource<TemplateSummary>(draftState, action)
return
}
case ADD_TEMPLATE_SUMMARY: {
addResource<TemplateSummary>(draftState, action, ResourceType.Templates)
return
}
case TOGGLE_TEMPLATE_RESOURCE_INSTALL: {
const {resourceType, shouldInstall, templateMetaName} = action

View File

@ -71,9 +71,6 @@ export const getLabelRelationships = (resource: {
return [].concat(resource.relationships[TemplateType.Label].data)
}
export const getIncludedLabels = (included: {type: TemplateType}[]) =>
included.filter((i): i is LabelIncluded => i.type === TemplateType.Label)
export interface TemplateDetails {
directory: string
templateExtension: string

View File

@ -4,7 +4,6 @@ import {get} from 'lodash'
// Actions
import {notify} from 'src/shared/actions/notifications'
import {setExportTemplate} from 'src/templates/actions/creators'
import {
setVariables,
setVariable,
@ -20,7 +19,6 @@ import {variableSchema, arrayOfVariables} from 'src/schemas/variables'
// APIs
import * as api from 'src/client'
import {hydrateVars} from 'src/variables/utils/hydrateVars'
import {createVariableFromTemplate as createVariableFromTemplateAJAX} from 'src/templates/api'
// Utils
import {
@ -29,8 +27,6 @@ import {
getAllVariables as getAllVariablesFromState,
normalizeValues,
} from 'src/variables/selectors'
import {variableToTemplate} from 'src/shared/utils/resourceToTemplate'
import {findDependentVariables} from 'src/variables/utils/exportVariables'
import {getOrg} from 'src/organizations/selectors'
import {getLabels, getStatus} from 'src/resources/selectors'
import {currentContext} from 'src/shared/selectors/currentContext'
@ -45,7 +41,6 @@ import {
AppState,
GetState,
RemoteDataState,
VariableTemplate,
Label,
GenVariable,
Variable,
@ -259,27 +254,6 @@ export const createVariable = (
}
}
export const createVariableFromTemplate = (
template: VariableTemplate
) => async (dispatch: Dispatch<Action>, getState: GetState) => {
try {
const state = getState()
const org = getOrg(state)
const resp = await createVariableFromTemplateAJAX(template, org.id)
const createdVar = normalize<Variable, VariableEntities, string>(
resp,
variableSchema
)
dispatch(setVariable(resp.id, RemoteDataState.Done, createdVar))
dispatch(notify(copy.createVariableSuccess(resp.name)))
} catch (error) {
console.error(error)
dispatch(notify(copy.createVariableFailed(error.message)))
}
}
export const updateVariable = (id: string, props: Variable) => async (
dispatch: Dispatch<Action>,
getState: GetState
@ -342,49 +316,6 @@ export const moveVariable = (originalIndex: number, newIndex: number) => async (
await dispatch(moveVariableInState(originalIndex, newIndex, contextID))
}
export const convertToTemplate = (variableID: string) => async (
dispatch,
getState: GetState
): Promise<void> => {
try {
dispatch(setExportTemplate(RemoteDataState.Loading))
const state = getState()
const org = getOrg(state)
const resp = await api.getVariable({variableID})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
const allVariables = await api.getVariables({query: {orgID: org.id}})
if (allVariables.status !== 200) {
throw new Error(allVariables.data.message)
}
const normVariable = normalize<Variable, VariableEntities, string>(
resp.data,
variableSchema
)
const normVariables = normalize<Variable, VariableEntities, string>(
allVariables.data.variables,
arrayOfVariables
)
const variable = normVariable.entities.variables[normVariable.result]
const variables = Object.values(normVariables.entities.variables)
const dependencies = findDependentVariables(variable, variables)
const variableTemplate = variableToTemplate(state, variable, dependencies)
dispatch(setExportTemplate(RemoteDataState.Done, variableTemplate))
} catch (error) {
dispatch(setExportTemplate(RemoteDataState.Error))
dispatch(notify(copy.createTemplateFailed(error)))
}
}
export const addVariableLabelAsync = (
variableID: string,
label: Label

View File

@ -1,63 +0,0 @@
import React, {PureComponent} from 'react'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import {connect, ConnectedProps} from 'react-redux'
// Components
import ExportOverlay from 'src/shared/components/ExportOverlay'
// Actions
import {convertToTemplate as convertToTemplateAction} from 'src/variables/actions/thunks'
import {clearExportTemplate as clearExportTemplateAction} from 'src/templates/actions/thunks'
// Types
import {AppState} from 'src/types'
type ReduxProps = ConnectedProps<typeof connector>
type Props = RouteComponentProps<{orgID: string; id: string}> & ReduxProps
class VariableExportOverlay extends PureComponent<Props> {
public componentDidMount() {
const {
match: {
params: {id},
},
convertToTemplate,
} = this.props
convertToTemplate(id)
}
public render() {
const {variableTemplate, status} = this.props
return (
<ExportOverlay
resourceName="Variable"
resource={variableTemplate}
onDismissOverlay={this.onDismiss}
status={status}
/>
)
}
private onDismiss = () => {
const {history, clearExportTemplate} = this.props
history.goBack()
clearExportTemplate()
}
}
const mstp = (state: AppState) => ({
variableTemplate: state.resources.templates.exportTemplate.item,
status: state.resources.templates.exportTemplate.status,
})
const mdtp = {
convertToTemplate: convertToTemplateAction,
clearExportTemplate: clearExportTemplateAction,
}
const connector = connect(mstp, mdtp)
export default connector(withRouter(VariableExportOverlay))

View File

@ -1,85 +0,0 @@
import React, {PureComponent} from 'react'
import {withRouter, RouteComponentProps} from 'react-router-dom'
import {connect, ConnectedProps} from 'react-redux'
// Components
import ImportOverlay from 'src/shared/components/ImportOverlay'
// Copy
import {invalidJSON} from 'src/shared/copy/notifications'
// Actions
import {
createVariableFromTemplate as createVariableFromTemplateAction,
getVariables as getVariablesAction,
} from 'src/variables/actions/thunks'
import {notify as notifyAction} from 'src/shared/actions/notifications'
// Types
import {ComponentStatus} from '@influxdata/clockface'
// Utils
import jsonlint from 'jsonlint-mod'
interface State {
status: ComponentStatus
}
type ReduxProps = ConnectedProps<typeof connector>
type Props = RouteComponentProps<{orgID: string}> & ReduxProps
class VariableImportOverlay extends PureComponent<Props> {
public state: State = {
status: ComponentStatus.Default,
}
public render() {
return (
<ImportOverlay
onDismissOverlay={this.onDismiss}
resourceName="Variable"
onSubmit={this.handleImportVariable}
status={this.state.status}
updateStatus={this.updateOverlayStatus}
/>
)
}
private onDismiss = () => {
const {history} = this.props
history.goBack()
}
private updateOverlayStatus = (status: ComponentStatus) =>
this.setState(() => ({status}))
private handleImportVariable = (uploadContent: string) => {
const {createVariableFromTemplate, getVariables, notify} = this.props
let template
this.updateOverlayStatus(ComponentStatus.Default)
try {
template = jsonlint.parse(uploadContent)
} catch (error) {
this.updateOverlayStatus(ComponentStatus.Error)
notify(invalidJSON(error.message))
return
}
createVariableFromTemplate(template)
getVariables()
this.onDismiss()
}
}
const mdtp = {
createVariableFromTemplate: createVariableFromTemplateAction,
getVariables: getVariablesAction,
notify: notifyAction,
}
const connector = connect(null, mdtp)
export default connector(withRouter(VariableImportOverlay))

View File

@ -13,13 +13,13 @@ import SearchWidget from 'src/shared/components/search_widget/SearchWidget'
import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader'
import VariableList from 'src/variables/components/VariableList'
import Filter from 'src/shared/components/FilterList'
import AddResourceDropdown from 'src/shared/components/AddResourceDropdown'
import AddResourceButton from 'src/shared/components/AddResourceButton'
import ResourceSortDropdown from 'src/shared/components/resource_sort_dropdown/ResourceSortDropdown'
import GetResources from 'src/resources/components/GetResources'
import {Sort} from '@influxdata/clockface'
// Types
import {AppState, OverlayState, ResourceType, Variable} from 'src/types'
import {AppState, ResourceType, Variable} from 'src/types'
import {ComponentSize} from '@influxdata/clockface'
import {SortTypes} from 'src/shared/utils/sort'
import {VariableSortKey} from 'src/shared/components/resource_sort_dropdown/generateSortItems'
@ -29,7 +29,6 @@ type Props = RouteComponentProps<{orgID: string}> & ReduxProps
interface State {
searchTerm: string
importOverlayState: OverlayState
sortKey: VariableSortKey
sortDirection: Sort
sortType: SortTypes
@ -40,7 +39,6 @@ const FilterList = Filter<Variable>()
class VariablesTab extends PureComponent<Props, State> {
public state: State = {
searchTerm: '',
importOverlayState: OverlayState.Closed,
sortKey: 'name',
sortDirection: Sort.Ascending,
sortType: SortTypes.String,
@ -68,9 +66,8 @@ class VariablesTab extends PureComponent<Props, State> {
)
const rightHeaderItems = (
<AddResourceDropdown
<AddResourceButton
resourceName="Variable"
onSelectImport={this.handleOpenImportOverlay}
onSelectNew={this.handleOpenCreateOverlay}
/>
)
@ -121,9 +118,8 @@ class VariablesTab extends PureComponent<Props, State> {
<EmptyState.Text>
Looks like there aren't any <b>Variables</b>, why not create one?
</EmptyState.Text>
<AddResourceDropdown
<AddResourceButton
resourceName="Variable"
onSelectImport={this.handleOpenImportOverlay}
onSelectNew={this.handleOpenCreateOverlay}
/>
</EmptyState>
@ -145,12 +141,6 @@ class VariablesTab extends PureComponent<Props, State> {
this.setState({searchTerm})
}
private handleOpenImportOverlay = (): void => {
const {history, match} = this.props
history.push(`/orgs/${match.params.orgID}/settings/variables/import`)
}
private handleOpenCreateOverlay = (): void => {
const {history, match} = this.props

View File

@ -10,8 +10,6 @@ import SettingsHeader from 'src/settings/components/SettingsHeader'
import {Page} from '@influxdata/clockface'
import VariablesTab from 'src/variables/components/VariablesTab'
import GetResources from 'src/resources/components/GetResources'
import VariableImportOverlay from 'src/variables/components/VariableImportOverlay'
import VariableExportOverlay from 'src/variables/components/VariableExportOverlay'
import CreateVariableOverlay from 'src/variables/components/CreateVariableOverlay'
import RenameVariableOverlay from 'src/variables/components/RenameVariableOverlay'
import UpdateVariableOverlay from 'src/variables/components/UpdateVariableOverlay'
@ -46,14 +44,6 @@ class VariablesIndex extends Component<StateProps> {
</SettingsTabbedPage>
</Page>
<Switch>
<Route
path={`${varsPath}/import`}
component={VariableImportOverlay}
/>
<Route
path={`${varsPath}/:id/export`}
component={VariableExportOverlay}
/>
<Route path={`${varsPath}/new`} component={CreateVariableOverlay} />
<Route
path={`${varsPath}/:id/rename`}

View File

@ -1,65 +0,0 @@
import {exportVariables} from 'src/variables/utils/exportVariables'
// Mocks
import {createVariable} from 'src/variables/mocks'
describe('exportVariables', () => {
it('should find dependent variables', () => {
const a = createVariable('a', 'f(x: v.b)')
const b = createVariable('b', 'cool')
const c = createVariable('c', 'nooo!')
const vars = [a, b, c]
const actual = exportVariables([a], vars)
expect(actual).toEqual([a, b])
})
it('should find dependent variables with cycles', () => {
const a = createVariable('a', 'f(x: v.b, y: v.c)')
const b = createVariable('b', 'f(x: v.f, y: v.e)')
const c = createVariable('c', 'f(x: v.g)')
const d = createVariable('d', 'nooooo!')
const e = createVariable('e', 'pick')
const f = createVariable('f', 'f(x: v.a, y: v.b)')
const g = createVariable('g', 'yay')
const h = createVariable('h', 'nooooo!')
const vars = [a, b, c, d, e, f, g, h]
const actual = exportVariables([a], vars)
const expected = new Set([a, b, c, e, f, g])
expect(new Set(actual)).toEqual(expected)
})
const examples = [
createVariable('alone', 'v.target'),
createVariable('space', '\tv.target\n'),
createVariable('func', 'f(x: v.target)'),
createVariable('brackets', '[v.target, other]'),
createVariable('braces', '(v.target)'),
createVariable('add', '1+v.target-2'),
createVariable('mult', '1*v.target/2'),
createVariable('mod', '1+v.target%2'),
createVariable('bool', '1>v.target<2'),
createVariable('assignment', 'x=v.target\n'),
createVariable('curly', '{beep:v.target}\n'),
createVariable('arrow', '(r)=>v.target==r.field\n'),
createVariable('comment', '\nv.target//wat?'),
createVariable('not equal', 'v.target!=r.field'),
createVariable('like', 'other=~v.target'),
]
examples.forEach(example => {
it(`should filter vars with shared prefix: ${example.name}`, () => {
const target = createVariable('target', 'match me!')
const partial = createVariable('tar', 'broke!')
const vars = [example, target, partial]
const actual = exportVariables([example], vars)
expect(actual).toEqual([example, target])
})
})
})

View File

@ -1,46 +0,0 @@
// Utils
import {
collectDescendants,
createVariableGraph,
VariableNode,
} from 'src/variables/utils/hydrateVars'
// Types
import {Variable} from 'src/types'
const getDescendantsFromGraph = (
variable: Variable,
varGraph: VariableNode[]
): Variable[] => {
const node = varGraph.find(n => n.variable.id === variable.id)
return collectDescendants(node).map(n => n.variable)
}
export const findDependentVariables = (
variable: Variable,
allVariables: Variable[]
) => {
const varGraph = createVariableGraph(allVariables)
return getDescendantsFromGraph(variable, varGraph)
}
export const exportVariables = (
variables: Variable[],
allVariables: Variable[]
): Variable[] => {
const varSet = new Set<Variable>()
const varGraph = createVariableGraph(allVariables)
for (const v of variables) {
if (varSet.has(v)) {
continue
}
varSet.add(v)
for (const d of getDescendantsFromGraph(v, varGraph)) {
varSet.add(d)
}
}
return [...varSet]
}