Skip to content

Commit

Permalink
feat(admin): enable features from ui
Browse files Browse the repository at this point in the history
  • Loading branch information
allardy committed Sep 17, 2019
1 parent 70d29d3 commit 5c33ed2
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 66 deletions.
6 changes: 6 additions & 0 deletions src/bp/common/typings.ts
Expand Up @@ -121,3 +121,9 @@ export interface FlowPoint {
}

export type NodeView = FlowNode & FlowPoint

export interface ServerConfig {
config: BotpressConfig
env: { [keyName: string]: string }
live: { [keyName: string]: string }
}
1 change: 1 addition & 0 deletions src/bp/core/routers/admin/index.ts
Expand Up @@ -58,6 +58,7 @@ export class AdminRouter extends CustomRouter {
this.serverRouter = new ServerRouter(
logger,
monitoringService,
workspaceService,
alertingService,
configProvider,
ghostService,
Expand Down
52 changes: 52 additions & 0 deletions src/bp/core/routers/admin/server.ts
Expand Up @@ -4,6 +4,7 @@ import { GhostService } from 'core/services'
import { AlertingService } from 'core/services/alerting-service'
import { JobService } from 'core/services/job-service'
import { MonitoringService } from 'core/services/monitoring'
import { WorkspaceService } from 'core/services/workspace-service'
import { Router } from 'express'
import _ from 'lodash'

Expand All @@ -16,6 +17,7 @@ export class ServerRouter extends CustomRouter {
constructor(
private logger: Logger,
private monitoringService: MonitoringService,
private workspaceService: WorkspaceService,
private alertingService: AlertingService,
private configProvider: ConfigProvider,
private ghostService: GhostService,
Expand Down Expand Up @@ -126,6 +128,56 @@ export class ServerRouter extends CustomRouter {
})
)

router.get(
'/serverConfig',
this.asyncMiddleware(async (req, res) => {
if (process.core_env.BP_DISABLE_SERVER_CONFIG) {
return res.send(undefined)
}

const serverConfig = {
config: await this.configProvider.getBotpressConfig(),
live: _.pick(process, ['EXTERNAL_URL', 'ROOT_PATH', 'PROXY', 'APP_DATA_PATH', 'IS_LICENSED']),
env: _.pick(process.core_env, [
'BPFS_STORAGE',
'PRO_ENABLED',
'REDIS_URL',
'EXTERNAL_URL',
'DATABASE_URL',
'BP_PRODUCTION',
'CLUSTER_ENABLED',
'AUTO_MIGRATE',
'BP_LICENSE_KEY'
])
}
res.send(serverConfig)
})
)

router.post(
'/features/enable/:featureId',
this.asyncMiddleware(async (req, res) => {
const { featureId } = req.params

if (featureId === 'pro') {
await this.configProvider.mergeBotpressConfig({ pro: { enabled: true } })
} else if (featureId === 'monitoring') {
await this.configProvider.mergeBotpressConfig({ pro: { monitoring: { enabled: true } } })
} else if (featureId === 'alerting') {
await this.configProvider.mergeBotpressConfig({ pro: { alerting: { enabled: true } } })
}

res.sendStatus(200)
})
)

router.get(
'/workspaces',
this.asyncMiddleware(async (req, res) => {
res.send(await this.workspaceService.getWorkspaces())
})
)

this._rebootServer = await this.jobService.broadcast<void>(this.__local_rebootServer.bind(this))
}

Expand Down
107 changes: 107 additions & 0 deletions src/bp/ui-admin/src/Pages/Components/CheckRequirements.tsx
@@ -0,0 +1,107 @@
import { Button, Callout } from '@blueprintjs/core'
import { ServerConfig } from 'common/typings'
import _ from 'lodash'
import React from 'react'
import { FC, useEffect, useState } from 'react'
import { connect } from 'react-redux'
import api from '~/api'
import { toastFailure, toastSuccess } from '~/utils/toaster'

import { fetchServerConfig } from '../../reducers/server'

type Feature = 'redis' | 'pro' | 'monitoring' | 'alerting'

interface Props {
requirements: Feature[]
feature: Feature
children: React.ReactNode

messageReqMissing?: string
messageDisabled?: string

serverConfig?: ServerConfig
serverConfigLoaded?: boolean
fetchServerConfig: () => void
}

const featureStatus = (serverCfg: ServerConfig) => {
return {
redis: !!(_.get(serverCfg, 'env.REDIS_URL') && _.get(serverCfg, 'env.CLUSTER_ENABLED') === 'true'),
pro: _.get(serverCfg, 'config.pro.enabled') || _.get(serverCfg, 'env.PRO_ENABLED') === 'true',
monitoring: _.get(serverCfg, 'config.pro.monitoring.enabled'),
alerting: _.get(serverCfg, 'config.pro.alerting.enabled')
}
}

const CheckRequirements: FC<Props> = props => {
const [isEnabled, setEnabled] = useState(false)
const [missingReq, setMissingReq] = useState<Feature[]>([])

useEffect(() => {
if (props.serverConfigLoaded) {
// Config is disabled, nothing returned from server. Skip requirement check
if (!props.serverConfig) {
setEnabled(true)
} else {
checkRequirements()
}
} else {
props.fetchServerConfig()
}
}, [props.serverConfig])

const checkRequirements = () => {
const status = featureStatus(props.serverConfig!)
setMissingReq(props.requirements.filter(x => !status[x]))
setEnabled(status[props.feature])
}

const enableFeature = async () => {
try {
await api.getSecured().post(`/admin/server/features/enable/${props.feature}`)
toastSuccess(`Configuration updated successfully! Restart Botpress for changes to take effect`)
props.fetchServerConfig()
} catch (err) {
toastFailure(`Could not update configuration: ${err.message}`)
}
}

if (missingReq.length) {
return (
<div className="requirements">
<Callout title="Missing requirements">
{props.messageReqMissing || 'To use this feature, these features are also required:'}
<div style={{ padding: '20px 0' }}>
{missingReq.includes('redis') && <strong>Redis must be enabled and correctly configured</strong>}
{missingReq.includes('pro') && <strong>Botpress Pro must be enabled with a valid license</strong>}
{missingReq.includes('monitoring') && <strong>Monitoring must be enabled</strong>}
</div>
</Callout>
</div>
)
}

return isEnabled ? (
<div>{props.children}</div>
) : (
<div className="requirements">
<Callout title="Feature disabled">
{props.messageDisabled ||
'To use this feature, click on the button below. A server restart will be required for changes to take effect.'}
<div style={{ padding: '20px 0' }}>
<Button onClick={enableFeature} text="Enable this feature" />
</div>
</Callout>
</div>
)
}

const mapStateToProps = state => ({
serverConfig: state.server.serverConfig,
serverConfigLoaded: state.server.serverConfigLoaded
})

export default connect(
mapStateToProps,
{ fetchServerConfig }
)(CheckRequirements)
11 changes: 10 additions & 1 deletion src/bp/ui-admin/src/Pages/Server/Alerting.jsx
Expand Up @@ -10,6 +10,7 @@ import SectionLayout from '../Layouts/Section'
import IncidentsTable from '../Components/Monitoring/IncidentsTable'
import LoadingSection from '../Components/LoadingSection'
import { fetchIncidents } from '../../reducers/monitoring'
import CheckRequirements from '../Components/CheckRequirements'

const timeFrameOptions = [
{ value: '1h', label: '1 hour' },
Expand Down Expand Up @@ -102,7 +103,7 @@ class Alerts extends Component {
)
}

render() {
renderChild() {
if (!this.props.incidents) {
return this.renderNoData()
}
Expand Down Expand Up @@ -150,6 +151,14 @@ class Alerts extends Component {
</div>
)
}

render() {
return (
<CheckRequirements requirements={['redis', 'pro', 'monitoring']} feature="alerting">
{this.renderChild()}
</CheckRequirements>
)
}
}

const mapStateToProps = state => ({
Expand Down

0 comments on commit 5c33ed2

Please sign in to comment.