diff --git a/Dockerfile b/Dockerfile index 8ce7ded8..957af7a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,8 @@ FROM node:8.17.0-jessie as builder WORKDIR /usr/build +ENV NODE_ENV=production +ENV NPM_CONFIG_PRODUCTION=true # Install app dependencies COPY package*.json ./ @@ -16,7 +18,6 @@ RUN mkdir -p ./app/protobuf/src && mv protobuf/src ./app/protobuf # Build final image FROM node:8.17.0-jessie-slim WORKDIR /usr/app -ENV NODE_ENV=production COPY --from=builder ./usr/build/app ./ EXPOSE 8088 CMD [ "node", "server.js" ] \ No newline at end of file diff --git a/.babelrc b/babel.config.js similarity index 83% rename from .babelrc rename to babel.config.js index d37bc976..518109d2 100644 --- a/.babelrc +++ b/babel.config.js @@ -1,4 +1,4 @@ -{ +module.exports = { "plugins": [ [ "module-resolver", @@ -9,7 +9,8 @@ "~helpers": "./client/helpers" } } - ] + ], + ["@babel/plugin-transform-regenerator"] ], "presets": [ [ @@ -22,4 +23,4 @@ ], "@babel/preset-env" ] -} +}; diff --git a/client/App.vue b/client/App.vue index ad3d0b59..ea71b229 100644 --- a/client/App.vue +++ b/client/App.vue @@ -1,15 +1,38 @@ + + diff --git a/client/components/error-message.vue b/client/components/error-message.vue new file mode 100644 index 00000000..9b04738a --- /dev/null +++ b/client/components/error-message.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/client/components/feature-flag/helpers.js b/client/components/feature-flag/helpers.js new file mode 100644 index 00000000..fa11228f --- /dev/null +++ b/client/components/feature-flag/helpers.js @@ -0,0 +1,10 @@ +export const isFlagEnabled = ({ flagHash = {}, name = '' }) => + flagHash[name] || false; + +export const mapFlagsToHash = (flagArray = []) => { + return flagArray.reduce((accumulator, { key = '', value = false }) => { + accumulator[key] = value; + + return accumulator; + }, {}); +}; diff --git a/client/components/feature-flag/helpers.spec.js b/client/components/feature-flag/helpers.spec.js new file mode 100644 index 00000000..af6cbc03 --- /dev/null +++ b/client/components/feature-flag/helpers.spec.js @@ -0,0 +1,38 @@ +import { isFlagEnabled, mapFlagsToHash } from './helpers'; + +describe('feature-flag helpers', () => { + describe('isFlagEnabled', () => { + it('should return false when passed name = "workflow-terminate" and flagHash = {}.', () => { + const name = 'workflow-terminate'; + const flagHash = {}; + const output = isFlagEnabled({ flagHash, name }); + + expect(output).toEqual(false); + }); + + it('should return true when passed name = "workflow-terminate" and flagHash = { "workflow-terminate": true }.', () => { + const name = 'workflow-terminate'; + const flagHash = { 'workflow-terminate': true }; + const output = isFlagEnabled({ flagHash, name }); + + expect(output).toEqual(true); + }); + + it('should return false when passed name = "workflow-terminate" and flagHash = { "workflow-terminate": false }.', () => { + const name = 'workflow-terminate'; + const flagHash = { 'workflow-terminate': false }; + const output = isFlagEnabled({ flagHash, name }); + + expect(output).toEqual(false); + }); + }); + + describe('mapFlagsToHash', () => { + it('should return { "workflow-terminate": true } when passed flagArray = [{ key: "workflow-terminate", value: true }].', () => { + const flagArray = [{ key: 'workflow-terminate', value: true }]; + const output = mapFlagsToHash(flagArray); + + expect(output).toEqual({ 'workflow-terminate': true }); + }); + }); +}); diff --git a/client/components/feature-flag/index.vue b/client/components/feature-flag/index.vue new file mode 100644 index 00000000..698f51b5 --- /dev/null +++ b/client/components/feature-flag/index.vue @@ -0,0 +1,25 @@ + + + diff --git a/client/components/flex-grid-item.vue b/client/components/flex-grid-item.vue new file mode 100644 index 00000000..7570062e --- /dev/null +++ b/client/components/flex-grid-item.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/client/components/flex-grid.vue b/client/components/flex-grid.vue new file mode 100644 index 00000000..2ee270ed --- /dev/null +++ b/client/components/flex-grid.vue @@ -0,0 +1,13 @@ + + + diff --git a/client/components/index.js b/client/components/index.js index caf2cfc9..458cfef9 100644 --- a/client/components/index.js +++ b/client/components/index.js @@ -1,9 +1,18 @@ export { default as BarLoader } from './bar-loader'; +export { default as ButtonFill } from './button-fill'; export { default as Copy } from './copy'; export { default as DataViewer } from './data-viewer'; export { default as DetailList } from './detail-list'; export { default as DateRangePicker } from './date-range-picker'; export { default as NamespaceNavigation } from './namespace-navigation'; +export { default as ErrorMessage } from './error-message'; +export { default as FeatureFlag } from './feature-flag'; +export { default as FlexGrid } from './flex-grid'; +export { default as FlexGridItem } from './flex-grid-item'; +export { default as LoadingMessage } from './loading-message'; +export { default as LoadingSpinner } from './loading-spinner'; export { default as NavigationBar } from './navigation-bar'; export { default as NavigationLink } from './navigation-link'; +export { default as NoResults } from './no-results'; export { default as NotificationBar } from './notification-bar'; +export { default as TextInput } from './text-input'; diff --git a/client/components/loading-message.vue b/client/components/loading-message.vue new file mode 100644 index 00000000..60ccb2c3 --- /dev/null +++ b/client/components/loading-message.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/client/components/loading-spinner.vue b/client/components/loading-spinner.vue new file mode 100644 index 00000000..b631466f --- /dev/null +++ b/client/components/loading-spinner.vue @@ -0,0 +1,22 @@ + + + diff --git a/client/components/no-results.vue b/client/components/no-results.vue new file mode 100644 index 00000000..abe8f270 --- /dev/null +++ b/client/components/no-results.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/client/components/text-input.vue b/client/components/text-input.vue new file mode 100644 index 00000000..bc8f29b2 --- /dev/null +++ b/client/components/text-input.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/client/constants.js b/client/constants.js index 8bfaa441..d0df70ed 100644 --- a/client/constants.js +++ b/client/constants.js @@ -1,6 +1,29 @@ export const jsonKeys = ['result', 'input', 'details', 'data', 'Error']; export const preKeys = jsonKeys.concat(['stackTrace', 'details.stackTrace']); +export const ENVIRONMENT_LIST = [ + // Make sure to enable "environment-select" in feature-flags.json to enable environment select. + // + // Examples: + // + // { + // label: 'Production', + // value: 'http://.com', + // }, + // { + // label: 'Staging', + // value: 'http://.com', + // }, + // { + // label: 'Development', + // value: 'http://.com', + // }, + // { + // label: 'Localhost', + // value: 'http://localhost:8088', + // }, +]; + export const MAXIMUM_JSON_CHARACTER_LIMIT = 5000; export const MAXIMUM_JSON_MESSAGE = '\n ... to see more open full screen mode from top right arrow.'; diff --git a/client/feature-flags.json b/client/feature-flags.json new file mode 100644 index 00000000..811821a4 --- /dev/null +++ b/client/feature-flags.json @@ -0,0 +1,10 @@ +[ + { + "key": "environment-select", + "value": false + }, + { + "key": "workflow-terminate", + "value": true + } +] diff --git a/client/helpers/get-end-time-iso-string.js b/client/helpers/get-end-time-iso-string.js new file mode 100644 index 00000000..8fc6b91d --- /dev/null +++ b/client/helpers/get-end-time-iso-string.js @@ -0,0 +1,13 @@ +import moment from 'moment'; + +export default (range, endTimeString) => { + if (range && typeof range === 'string') { + const [, , unit] = range.split('-'); + + return moment() + .endOf(unit) + .toISOString(); + } + + return endTimeString; +}; diff --git a/client/helpers/get-end-time-iso-string.spec.js b/client/helpers/get-end-time-iso-string.spec.js new file mode 100644 index 00000000..0b1da3a6 --- /dev/null +++ b/client/helpers/get-end-time-iso-string.spec.js @@ -0,0 +1,26 @@ +import getEndTimeIsoString from './get-end-time-iso-string'; + +describe('getEndTimeIsoString', () => { + describe('When range = undefined', () => { + it('should return endTimeString.', () => { + const range = undefined; + const endTimeString = '2020-03-30T00:00:00Z'; + const output = getEndTimeIsoString(range, endTimeString); + + expect(output).toEqual(endTimeString); + }); + }); + + describe('When moment is set to March 1st 2020 and range = "last-30-days"', () => { + it('should return "2020-03-02T07:59:59.999Z".', () => { + jest + .spyOn(Date, 'now') + .mockImplementation(() => new Date(2020, 2, 1).getTime()); + const range = 'last-30-days'; + const endTimeString = ''; + const output = getEndTimeIsoString(range, endTimeString); + + expect(output).toEqual('2020-03-02T07:59:59.999Z'); + }); + }); +}); diff --git a/client/helpers/get-environment-list.js b/client/helpers/get-environment-list.js new file mode 100644 index 00000000..824db989 --- /dev/null +++ b/client/helpers/get-environment-list.js @@ -0,0 +1,2 @@ +export default ({ environmentList, origin }) => + environmentList.filter(({ value }) => value !== origin); diff --git a/client/helpers/get-environment-list.spec.js b/client/helpers/get-environment-list.spec.js new file mode 100644 index 00000000..61ff23cd --- /dev/null +++ b/client/helpers/get-environment-list.spec.js @@ -0,0 +1,27 @@ +import getEnvironmentList from './get-environment-list'; + +describe('getEnvironmentList', () => { + it('should exclude the current environment from the list.', () => { + const environmentList = [ + { + value: 'https://production-environment.com', + }, + { + value: 'https://staging-environment.com', + }, + { + value: 'https://development-environment.com', + }, + ]; + const origin = 'https://production-environment.com'; + const output = getEnvironmentList({ environmentList, origin }); + + expect(output.length).toEqual(2); + expect(output[0]).toEqual({ + value: 'https://staging-environment.com', + }); + expect(output[1]).toEqual({ + value: 'https://development-environment.com', + }); + }); +}); diff --git a/client/helpers/get-environment-location.js b/client/helpers/get-environment-location.js new file mode 100644 index 00000000..c6bd468a --- /dev/null +++ b/client/helpers/get-environment-location.js @@ -0,0 +1,2 @@ +export default ({ environment: { value }, pathname = '', search = '' }) => + `${value}${pathname}${search}`; diff --git a/client/helpers/get-environment-location.spec.js b/client/helpers/get-environment-location.spec.js new file mode 100644 index 00000000..a52d65f2 --- /dev/null +++ b/client/helpers/get-environment-location.spec.js @@ -0,0 +1,17 @@ +import getEnvironmentLocation from './get-environment-location'; + +describe('getEnvironmentLocation', () => { + it(`should return "https://production-environment.com/path/to/page?query=hello" when passed + environment = { value: "https://production-environment.com" } + pathname = "/path/to/page" + search = "?query=hello"`, () => { + const environment = { value: 'https://production-environment.com' }; + const pathname = '/path/to/page'; + const search = '?query=hello'; + const output = getEnvironmentLocation({ environment, pathname, search }); + + expect(output).toEqual( + 'https://production-environment.com/path/to/page?query=hello' + ); + }); +}); diff --git a/client/helpers/get-environment.js b/client/helpers/get-environment.js new file mode 100644 index 00000000..79be6a35 --- /dev/null +++ b/client/helpers/get-environment.js @@ -0,0 +1,5 @@ +export default ({ environmentList, origin }) => + environmentList.find(({ value }) => value === origin) || { + label: 'Unknown', + value: origin, + }; diff --git a/client/helpers/get-environment.spec.js b/client/helpers/get-environment.spec.js new file mode 100644 index 00000000..02c43661 --- /dev/null +++ b/client/helpers/get-environment.spec.js @@ -0,0 +1,35 @@ +import getEnvironment from './get-environment'; + +describe('getEnvironment', () => { + const LOCALHOST_DOMAIN = 'http://localhost:8088'; + const LOCALHOST_OPTION = { + label: 'Localhost', + value: LOCALHOST_DOMAIN, + }; + + const UNKNOWN_DOMAIN = 'http://unknown.domain.com'; + const UNKNOWN_OPTION = { + label: 'Unknown', + value: UNKNOWN_DOMAIN, + }; + + const ENVIRONMENT_LIST = [LOCALHOST_OPTION]; + + it('should return UNKNOWN_OPTION when origin = UNKNOWN_DOMAIN and is not part of environmentList.', () => { + const output = getEnvironment({ + environmentList: ENVIRONMENT_LIST, + origin: UNKNOWN_DOMAIN, + }); + + expect(output).toEqual(UNKNOWN_OPTION); + }); + + it('should return LOCALHOST_OPTION when origin = LOCALHOST_DOMAIN and environmentList = [LOCALHOST_OPTION].', () => { + const output = getEnvironment({ + environmentList: ENVIRONMENT_LIST, + origin: LOCALHOST_DOMAIN, + }); + + expect(output).toEqual(LOCALHOST_OPTION); + }); +}); diff --git a/client/helpers/get-start-time-iso-string.js b/client/helpers/get-start-time-iso-string.js new file mode 100644 index 00000000..e56abdcf --- /dev/null +++ b/client/helpers/get-start-time-iso-string.js @@ -0,0 +1,14 @@ +import moment from 'moment'; + +export default (range, startTimeString) => { + if (range && typeof range === 'string') { + const [, count, unit] = range.split('-'); + + return moment() + .subtract(count, unit) + .startOf(unit) + .toISOString(); + } + + return startTimeString; +}; diff --git a/client/helpers/get-start-time-iso-string.spec.js b/client/helpers/get-start-time-iso-string.spec.js new file mode 100644 index 00000000..70a43007 --- /dev/null +++ b/client/helpers/get-start-time-iso-string.spec.js @@ -0,0 +1,26 @@ +import getStartTimeIsoString from './get-start-time-iso-string'; + +describe('getStartTimeIsoString', () => { + describe('When range = undefined and startTimeString = "2020-03-30T00:00:00Z"', () => { + it('should return "2020-03-30T00:00:00Z".', () => { + const range = undefined; + const startTimeString = '2020-03-30T00:00:00Z'; + const output = getStartTimeIsoString(range, startTimeString); + + expect(output).toEqual(startTimeString); + }); + }); + + describe('When moment is set to March 1st 2020 and range = "last-30-days"', () => { + it('should return "2020-01-31T08:00:00.000Z".', () => { + jest + .spyOn(Date, 'now') + .mockImplementation(() => new Date(2020, 2, 1).getTime()); + const range = 'last-30-days'; + const startTimeString = ''; + const output = getStartTimeIsoString(range, startTimeString); + + expect(output).toEqual('2020-01-31T08:00:00.000Z'); + }); + }); +}); diff --git a/client/helpers/index.js b/client/helpers/index.js index 96d50f5d..e32385b3 100644 --- a/client/helpers/index.js +++ b/client/helpers/index.js @@ -1,6 +1,11 @@ +export { default as getEndTimeIsoString } from './get-end-time-iso-string'; +export { default as getEnvironment } from './get-environment'; +export { default as getEnvironmentList } from './get-environment-list'; +export { default as getEnvironmentLocation } from './get-environment-location'; export { default as getErrorMessage } from './get-error-message'; export { default as getJsonStringObject } from './get-json-string-object'; export { default as getKeyValuePairs } from './get-key-value-pairs'; +export { default as getStartTimeIsoString } from './get-start-time-iso-string'; export { default as getStringElipsis } from './get-string-elipsis'; export { default as http } from './http'; export { default as injectMomentDurationFormat } from './inject-moment-duration-format'; diff --git a/client/helpers/map-namespace-description.js b/client/helpers/map-namespace-description.js index dcaea3d6..9da17345 100644 --- a/client/helpers/map-namespace-description.js +++ b/client/helpers/map-namespace-description.js @@ -1,22 +1,40 @@ -export default function(d) { - // eslint-disable-next-line no-param-reassign - d.configuration = d.configuration || {}; - // eslint-disable-next-line no-param-reassign - d.replicationConfiguration = d.replicationConfiguration || { clusters: [] }; +export default function(namespace) { + const { + configuration: { + emitMetric, + historyArchivalStatus, + workflowExecutionRetentionPeriodInDays, + visibilityArchivalStatus, + } = {}, + namespaceInfo: { description, ownerEmail } = {}, + failoverVersion, + isGlobalNamespace, + replicationConfiguration: { activeClusterName, clusters = [] } = {}, + } = namespace || {}; return { - description: d.namespaceInfo.description, - owner: d.namespaceInfo.ownerEmail, - 'Global?': d.isGlobalNamespace ? 'Yes' : 'No', - 'Retention Period': `${d.configuration.workflowExecutionRetentionPeriodInDays} days`, - 'Emit Metrics': d.configuration.emitMetric ? 'Yes' : 'No', - 'Failover Version': d.failoverVersion, - clusters: d.replicationConfiguration.clusters - .map(c => - c.clusterName === d.replicationConfiguration.activeClusterName - ? `${c.clusterName} (active)` - : c.clusterName - ) - .join(', '), + description: description || 'No description available', + owner: ownerEmail || 'Unknown', + 'Global?': isGlobalNamespace ? 'Yes' : 'No', + 'Retention Period': workflowExecutionRetentionPeriodInDays + ? `${workflowExecutionRetentionPeriodInDays} days` + : 'Unknown', + 'Emit Metrics': emitMetric ? 'Yes' : 'No', + 'History Archival': + historyArchivalStatus === 'ENABLED' ? 'Enabled' : 'Disabled', + 'Visibility Archival': + visibilityArchivalStatus === 'ENABLED' ? 'Enabled' : 'Disabled', + ...(failoverVersion !== undefined && { + 'Failover Version': failoverVersion, + }), + clusters: clusters.length + ? clusters + .map(({ clusterName }) => + clusterName === activeClusterName + ? `${clusterName} (active)` + : clusterName + ) + .join(', ') + : 'Unknown', }; } diff --git a/client/helpers/map-namespace-description.spec.js b/client/helpers/map-namespace-description.spec.js new file mode 100644 index 00000000..1a2dbb13 --- /dev/null +++ b/client/helpers/map-namespace-description.spec.js @@ -0,0 +1,182 @@ +import mapNamespaceDescription from './map-namespace-description'; + +describe('mapNamespaceDescription', () => { + describe('When namespace is empty', () => { + let namespace; + + beforeEach(() => { + namespace = undefined; + }); + + it('should return "Emit Metrics" = "No".', () => { + const output = mapNamespaceDescription(namespace); + + expect(output['Emit Metrics']).toEqual('No'); + }); + + it('should return "Global?" = "No".', () => { + const output = mapNamespaceDescription(namespace); + + expect(output['Global?']).toEqual('No'); + }); + + it('should return "History Archival" = "Disabled".', () => { + const output = mapNamespaceDescription(namespace); + + expect(output['History Archival']).toEqual('Disabled'); + }); + + it('should return "Retention Period" = "Unknown".', () => { + const output = mapNamespaceDescription(namespace); + + expect(output['Retention Period']).toEqual('Unknown'); + }); + + it('should return "Visibility Archival" = "Disabled".', () => { + const output = mapNamespaceDescription(namespace); + + expect(output['History Archival']).toEqual('Disabled'); + }); + + it('should return "clusters" = "Unknown".', () => { + const output = mapNamespaceDescription(namespace); + + expect(output.clusters).toEqual('Unknown'); + }); + + it('should return "description" = "No description available".', () => { + const output = mapNamespaceDescription(namespace); + + expect(output.description).toEqual('No description available'); + }); + + it('should return "owner" = "Unknown".', () => { + const output = mapNamespaceDescription(namespace); + + expect(output.owner).toEqual('Unknown'); + }); + }); + + describe('When namespace.namespaceInfo.description = "NamespaceDescription"', () => { + it('should return "description" = "NamespaceDescription".', () => { + const namespace = { + namespaceInfo: { + description: 'NamespaceDescription', + }, + }; + + const output = mapNamespaceDescription(namespace); + + expect(output.description).toEqual('NamespaceDescription'); + }); + }); + + describe('When namespace.namespaceInfo.ownerEmail = "OwnerEmail"', () => { + it('should return "owner" = "OwnerEmail".', () => { + const namespace = { + namespaceInfo: { + ownerEmail: 'OwnerEmail', + }, + }; + + const output = mapNamespaceDescription(namespace); + + expect(output.owner).toEqual('OwnerEmail'); + }); + }); + + describe('When namespace.isGlobalNamespace = true', () => { + it('should return "Global?" = "Yes".', () => { + const namespace = { + isGlobalNamespace: true, + }; + + const output = mapNamespaceDescription(namespace); + + expect(output['Global?']).toEqual('Yes'); + }); + }); + + describe('When namespace.configuration.workflowExecutionRetentionPeriodInDays = 3', () => { + it('should return "Retention Period" = "3 days".', () => { + const namespace = { + configuration: { + workflowExecutionRetentionPeriodInDays: 3, + }, + }; + + const output = mapNamespaceDescription(namespace); + + expect(output['Retention Period']).toEqual('3 days'); + }); + }); + + describe('When namespace.configuration.emitMetric = true', () => { + it('should return "Emit Metrics" = "Yes".', () => { + const namespace = { + configuration: { + emitMetric: true, + }, + }; + + const output = mapNamespaceDescription(namespace); + + expect(output['Emit Metrics']).toEqual('Yes'); + }); + }); + + describe('When namespace.configuration.historyArchivalStatus = "ENABLED"', () => { + it('should return "History Archival" = "Enabled".', () => { + const namespace = { + configuration: { + historyArchivalStatus: 'ENABLED', + }, + }; + + const output = mapNamespaceDescription(namespace); + + expect(output['History Archival']).toEqual('Enabled'); + }); + }); + + describe('When namespace.configuration.visibilityArchivalStatus = "ENABLED"', () => { + it('should return "Visibility Archival" = "Enabled".', () => { + const namespace = { + configuration: { + visibilityArchivalStatus: 'ENABLED', + }, + }; + + const output = mapNamespaceDescription(namespace); + + expect(output['Visibility Archival']).toEqual('Enabled'); + }); + }); + + describe('When namespace.failoverVersion = 1', () => { + it('should return "Failover Version" = 1', () => { + const namespace = { + failoverVersion: 1, + }; + + const output = mapNamespaceDescription(namespace); + + expect(output['Failover Version']).toEqual(1); + }); + }); + + describe(`Multiple clusters with one active cluster`, () => { + it('should return "clusters" = "cluster1 (active), cluster2".', () => { + const namespace = { + replicationConfiguration: { + activeClusterName: 'cluster1', + clusters: [{ clusterName: 'cluster1' }, { clusterName: 'cluster2' }], + }, + }; + + const output = mapNamespaceDescription(namespace); + + expect(output.clusters).toEqual('cluster1 (active), cluster2'); + }); + }); +}); diff --git a/client/main.js b/client/main.js index 3b39dca4..43d8910b 100644 --- a/client/main.js +++ b/client/main.js @@ -8,22 +8,27 @@ import qs from 'friendly-querystring'; import moment from 'moment'; import promiseFinally from 'promise.prototype.finally'; -import copyButton from './components/copy.vue'; +import copyButton from './components/copy'; import snapscroll from './directives/snapscroll'; -import App from './App.vue'; -import Root from './routes/index.vue'; -import Help from './routes/help/index.vue'; +import App from './App'; +import Namespace from './routes/namespace/index.vue'; import NamespaceList from './routes/namespace-list.vue'; -import WorkflowList from './routes/namespace/workflow-list.vue'; -import NamespaceConfig from './routes/namespace/namespace-config.vue'; -import WorkflowTabs from './routes/workflow/index.vue'; -import WorkflowSummary from './routes/workflow/summary.vue'; -import History from './routes/workflow/history.vue'; -import StackTrace from './routes/workflow/stack-trace.vue'; -import Query from './routes/workflow/query.vue'; -import TaskList from './routes/namespace/task-list.vue'; +import NamespaceSettings from './routes/namespace/namespace-settings.vue'; +import Help from './routes/help'; +import History from './routes/workflow/history'; +import Query from './routes/workflow/query'; +import Root from './routes'; +import StackTrace from './routes/workflow/stack-trace'; +import TaskList from './routes/namespace/task-list'; +import WorkflowArchival from './routes/namespace/workflow-archival'; +import WorkflowArchivalAdvanced from './routes/namespace/workflow-archival/advanced'; +import WorkflowArchivalBasic from './routes/namespace/workflow-archival/basic'; +import WorkflowList from './routes/namespace/workflow-list'; +import WorkflowSummary from './routes/workflow/summary'; +import WorkflowTabs from './routes/workflow'; + import { http, injectMomentDurationFormat, jsonTryParse } from '~helpers'; const routeOpts = { @@ -51,22 +56,53 @@ const routeOpts = { ], }, { - name: 'namespaces-redirect', - path: '/namespace/*', - redirect: '/namespaces/*', - }, - { - name: 'workflow-list', - path: '/namespaces/:namespace/workflows', - component: WorkflowList, - }, - { - name: 'namespace-config', - path: '/namespaces/:namespace/config', - component: NamespaceConfig, + name: 'namespace', + path: '/namespaces/:namespace', + redirect: '/namespaces/:namespace/workflows', + component: Namespace, props: ({ params }) => ({ namespace: params.namespace, }), + children: [ + { + name: 'workflow-list', + path: '/namespaces/:namespace/workflows', + components: { + 'workflow-list': WorkflowList, + }, + }, + { + name: 'namespace-settings', + path: '/namespaces/:namespace/settings', + components: { + 'namespace-settings': NamespaceSettings, + }, + }, + { + name: 'workflow-archival', + path: '/namespaces/:namespace/archival', + redirect: '/namespaces/:namespace/archival/basic', + components: { + 'workflow-archival': WorkflowArchival, + }, + children: [ + { + name: 'workflow-archival-advanced', + path: '/namespaces/:namespace/archival/advanced', + components: { + 'workflow-archival-advanced': WorkflowArchivalAdvanced, + }, + }, + { + name: 'workflow-archival-basic', + path: '/namespaces/:namespace/archival/basic', + components: { + 'workflow-archival-basic': WorkflowArchivalBasic, + }, + }, + ], + }, + ], }, { name: 'workflow', @@ -110,7 +146,8 @@ const routeOpts = { }, { name: 'workflow/stack-trace', - path: '/namespaces/:namespace/workflows/:workflowId/:runId/stack-trace', + path: + '/namespaces/:namespace/workflows/:workflowId/:runId/stack-trace', components: { stacktrace: StackTrace, }, @@ -129,6 +166,19 @@ const routeOpts = { path: '/namespaces/:namespace/task-lists/:taskList', component: TaskList, }, + + // redirects + + { + name: 'namespaces-redirect', + path: '/namespace/*', + redirect: '/namespaces/*', + }, + { + name: 'namespace-config-redirect', + path: '/namespaces/:namespace/config', + redirect: '/namespaces/:namespace/settings', + }, { path: '/namespaces/:namespace/history', redirect: ({ params, query }) => { diff --git a/client/routes/help/index.vue b/client/routes/help/index.vue index 614f5c78..665707a5 100644 --- a/client/routes/help/index.vue +++ b/client/routes/help/index.vue @@ -154,15 +154,6 @@ Ask a question on Stack Overflow -
- - Join our discussion group - -
+ + + + + + + + + + + + + + diff --git a/client/routes/namespace/namespace-service.js b/client/routes/namespace/namespace-service.js new file mode 100644 index 00000000..c219bc18 --- /dev/null +++ b/client/routes/namespace/namespace-service.js @@ -0,0 +1,9 @@ +import { http } from '~helpers'; + +export default () => { + return { + getNamespaceSettings: namespaceName => { + return http(window.fetch, `/api/namespaces/${namespaceName}`); + }, + }; +}; diff --git a/client/routes/namespace/namespace-config.vue b/client/routes/namespace/namespace-settings.vue similarity index 90% rename from client/routes/namespace/namespace-config.vue rename to client/routes/namespace/namespace-settings.vue index 9a991b51..3797f207 100644 --- a/client/routes/namespace/namespace-config.vue +++ b/client/routes/namespace/namespace-settings.vue @@ -1,5 +1,8 @@ @@ -110,9 +112,14 @@ import moment from 'moment'; import debounce from 'lodash-es/debounce'; import pagedGrid from '~components/paged-grid'; import { DateRangePicker } from '~components'; -import { timestampToDate } from '~helpers'; +import { + getEndTimeIsoString, + getStartTimeIsoString, + timestampToDate, +} from '~helpers'; export default pagedGrid({ + props: ['namespace'], data() { return { loading: true, @@ -134,13 +141,13 @@ export default pagedGrid({ }; }, created() { - this.$http(`/api/namespaces/${this.$route.params.namespace}`).then(r => { + this.$http(`/api/namespaces/${this.namespace}`).then(r => { this.maxRetentionDays = Number(r.configuration.workflowExecutionRetentionPeriodInDays) || 30; if (!this.isRouteRangeValid(this.minStartDate)) { const prevRange = localStorage.getItem( - `${this.$route.params.namespace}:workflows-time-range` + `${this.namespace}:workflows-time-range` ); if (prevRange && this.isRangeValid(prevRange, this.minStartDate)) { @@ -165,79 +172,104 @@ export default pagedGrid({ 'date-range-picker': DateRangePicker, }, computed: { - filterBy() { - return this.status.value === 'OPEN' ? 'StartTime' : 'CloseTime'; - }, - status() { - if (!this.$route.query || !this.$route.query.status) { - return this.statuses[0]; + fetchUrl() { + const { namespace, queryString, state } = this; + + if (queryString) { + return `/api/namespaces/${namespace}/workflows/list`; } - return this.statuses.find(s => s.value === this.$route.query.status); + return `/api/namespaces/${namespace}/workflows/${state}`; }, - range() { - const q = this.$route.query || {}; + endTime() { + const { endTime, range } = this.$route.query; - return q.startTime && q.endTime - ? { - startTime: moment(q.startTime), - endTime: moment(q.endTime), - } - : q.range; + return getEndTimeIsoString(range, endTime); }, - criteria() { - const { namespace } = this.$route.params; - const q = this.$route.query; - const startTime = this.getStartTimeIsoString(q.range, q.startTime); - const endTime = this.getEndTimeIsoString(q.range, q.endTime); - - this.nextPageToken = undefined; + filterBy() { + return this.status.value === 'OPEN' ? 'StartTime' : 'CloseTime'; + }, + startTime() { + const { range, startTime } = this.$route.query; - return { - namespace, - startTime, - endTime, - status: q.status, - workflowId: q.workflowId, - workflowName: q.workflowName, - queryString: q.queryString, - }; + return getStartTimeIsoString(range, startTime); }, - queryOnChange() { - const q = { ...this.criteria }; - const { namespace } = q; - const state = !q.status || q.status === 'OPEN' ? 'open' : 'closed'; + state() { + const { statusName } = this; - if (!q.startTime || !q.endTime || !q.status) { - return; - } + return !statusName || statusName === 'OPEN' ? 'open' : 'closed'; + }, + status() { + return !this.$route.query || !this.$route.query.status + ? this.statuses[0] + : this.statuses.find(s => s.value === this.$route.query.status); + }, + statusName() { + return this.status.value; + }, + range() { + const query = this.$route.query || {}; if (!this.isRouteRangeValid(this.minStartDate)) { const updatedQuery = this.setRange( `last-${Math.min(30, this.maxRetentionDays)}-days` ); - q.startTime = this.getStartTimeIsoString( + query.startTime = getStartTimeIsoString( updatedQuery.range, - q.startTime + query.startTime ); - q.endTime = this.getEndTimeIsoString(updatedQuery.range, q.endTime); + query.endTime = getEndTimeIsoString(updatedQuery.range, query.endTime); } - if (['OPEN', 'CLOSED'].includes(q.status)) { - delete q.status; + return query.startTime && query.endTime + ? { + startTime: moment(query.startTime), + endTime: moment(query.endTime), + } + : query.range; + }, + criteria() { + const { + endTime, + queryString, + startTime, + statusName: status, + workflowId, + workflowName, + } = this; + + this.nextPageToken = undefined; + + if (!startTime || !endTime) { + return null; } - delete q.namespace; - q.nextPageToken = this.nextPageToken; + const includeStatus = !['OPEN', 'CLOSED'].includes(status); - if (q.queryString) { - this.fetch(`/api/namespaces/${namespace}/workflows/list`, q); + const criteria = { + startTime, + endTime, + ...(queryString && { queryString }), + ...(includeStatus && { status }), + ...(workflowId && { workflowId }), + ...(workflowName && { workflowName }), + }; + return criteria; + }, + queryOnChange() { + if (!this.criteria) { return; } - this.fetch(`/api/namespaces/${namespace}/workflows/${state}`, q); + const { fetchUrl, nextPageToken } = this; + const query = { ...this.criteria, nextPageToken }; + + this.fetch(fetchUrl, query); + }, + queryString() { + return this.$route.query.queryString; }, minStartDate() { const { @@ -253,31 +285,14 @@ export default pagedGrid({ .subtract(maxRetentionDays, 'days') .startOf('days'); }, - }, - methods: { - getStartTimeIsoString(range, startTimeString) { - if (range && typeof range === 'string') { - const [, count, unit] = range.split('-'); - - return moment() - .subtract(count, unit) - .startOf(unit) - .toISOString(); - } - - return startTimeString; + workflowId() { + return this.$route.query.workflowId; }, - getEndTimeIsoString(range, endTimeString) { - if (range && typeof range === 'string') { - const [, , unit] = range.split('-'); - - return moment() - .endOf(unit) - .toISOString(); - } - - return endTimeString; + workflowName() { + return this.$route.query.workflowName; }, + }, + methods: { fetch: debounce( function fetch(url, query) { this.loading = true; @@ -377,8 +392,7 @@ export default pagedGrid({ return false; }, isRouteRangeValid(minStartDate) { - const q = this.$route.query || {}; - const { endTime, range, startTime } = q; + const { endTime, range, startTime } = this.$route.query || {}; if (range) { return this.isRangeValid(range, minStartDate); @@ -398,10 +412,7 @@ export default pagedGrid({ query.range = range; delete query.startTime; delete query.endTime; - localStorage.setItem( - `${this.$route.params.namespace}:workflows-time-range`, - range - ); + localStorage.setItem(`${this.namespace}:workflows-time-range`, range); } else { query.startTime = range.startTime.toISOString(); query.endTime = range.endTime.toISOString(); @@ -433,10 +444,27 @@ export default pagedGrid({ @require "../../styles/definitions.styl" section.workflow-list + display: flex; + flex: auto; + flex-direction: column; + .filters - flex-wrap wrap + display: flex; + flex-direction: row; + flex-wrap: wrap; + > .field flex 1 1 auto + margin-right: 5px; + + .date-range-picker { + margin-right: 5px; + } + + .dropdown { + margin-right: 5px; + } + .status { width: 160px; } @@ -448,6 +476,10 @@ section.workflow-list paged-grid() + section.results { + flex: auto; + } + &.loading section.results table opacity 0.7 diff --git a/client/routes/workflow/history.vue b/client/routes/workflow/history.vue index 7c0d6949..12894344 100644 --- a/client/routes/workflow/history.vue +++ b/client/routes/workflow/history.vue @@ -638,7 +638,7 @@ section.history width: 150px; & + .spacer width: 100%; - height: 60px; + height: 58px; .tr display: flex; flex: 1; diff --git a/client/routes/workflow/index.vue b/client/routes/workflow/index.vue index 23025975..d233ff56 100644 --- a/client/routes/workflow/index.vue +++ b/client/routes/workflow/index.vue @@ -1,5 +1,5 @@