refactor(templates): Remove legacy templates (#19300)
parent
a03745e57d
commit
543bbd12c1
|
@ -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) => {
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
|
@ -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))
|
|
@ -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;
|
||||
}
|
|
@ -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 = {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -1,3 +0,0 @@
|
|||
.export-overlay--text-area {
|
||||
height: 60vh;
|
||||
}
|
|
@ -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)
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 [
|
||||
{
|
||||
|
|
|
@ -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`}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
|
@ -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))
|
|
@ -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))
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
|
|
@ -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))
|
|
@ -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 => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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))
|
|
@ -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)
|
|
@ -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}
|
||||
/>
|
||||
))
|
||||
}
|
||||
}
|
|
@ -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))
|
|
@ -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))
|
|
@ -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))
|
|
@ -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))
|
|
@ -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}
|
||||
/>
|
||||
))
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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;
|
||||
}
|
|
@ -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))
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
|
@ -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
|
|
@ -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
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
|
@ -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))
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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`}
|
||||
|
|
|
@ -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])
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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]
|
||||
}
|
Loading…
Reference in New Issue