diff --git a/frontend/.eslintrc b/frontend/.eslintrc index 36921ca49..2f5e7b7af 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -15,7 +15,9 @@ "EXPORTER_GROUP": true, "OC_GROUP": true, "EXPORTER_MODE": true, - "ZONE": true, + "CODE_EXPORT_ENABLED": true, + "REPOSITORY_HOST": true, + "ZONE": true "HELP_URL": true }, "parser": "babel-eslint", diff --git a/frontend/config/default.json.example b/frontend/config/default.json.example index a27a770a4..9c5625e28 100644 --- a/frontend/config/default.json.example +++ b/frontend/config/default.json.example @@ -13,6 +13,7 @@ "ocGroup": "/oc", "exporterMode": "download", "codeExportEnabled": false, + "repositoryHost": "any code repository regex", "auth": { "authorizationEndpoint": "URL_REQUIRED", "callbackURL": "http://localhost:8000/auth", diff --git a/frontend/config/test.json.example b/frontend/config/test.json.example index 27b3675d9..670edbac9 100644 --- a/frontend/config/test.json.example +++ b/frontend/config/test.json.example @@ -11,8 +11,9 @@ "filesApiHost": "localhost:1080", "exporterGroup": "/exporter", "ocGroup": "/oc", - "exporterMode": "export", - "codeExportEnabled": true, + "exporterMode": "download", + "codeExportEnabled": false, + "repositoryHost": "any code repository regex", "auth": { "authorizationEndpoint": "URL_REQUIRED", "callbackURL": "http://localhost:8000/auth", diff --git a/frontend/entrypoint.sh b/frontend/entrypoint.sh index 733652b74..80d84074b 100644 --- a/frontend/entrypoint.sh +++ b/frontend/entrypoint.sh @@ -13,6 +13,7 @@ printf "\"exporterGroup\": \"${EXPORTER_GROUP}\",\n" >> ./config/default.json printf "\"ocGroup\": \"${OC_GROUP}\",\n" >> ./config/default.json printf "\"exporterMode\": \"${EXPORTER_MODE}\",\n" >> ./config/default.json printf "\"codeExportEnabled\": \"${CODE_EXPORT_ENABLED}\",\n" >> ./config/default.json +printf "\"repositoryHost\": \"${REPOSITORY_HOST}\",\n" >> ./config/default.json printf "\"cookieSecret\": \"${COOKIE_SECRET}\",\n" >> ./config/default.json printf "\"jwtSecret\": \"${JWT_SECRET}\",\n" >> ./config/default.json printf "\"auth\": {\n" >> ./config/default.json diff --git a/frontend/helm/ocwa-frontend/templates/deployment.yaml b/frontend/helm/ocwa-frontend/templates/deployment.yaml index 5d4a1fbfe..3331e6c03 100644 --- a/frontend/helm/ocwa-frontend/templates/deployment.yaml +++ b/frontend/helm/ocwa-frontend/templates/deployment.yaml @@ -46,6 +46,8 @@ spec: value: "export" - name: CODE_EXPORT_ENABLED value: "{{ .Values.codeExportEnabled }}" + - name: REPOSITORY_HOST + value: "{{ .Values.repositoryHost }}" - name: COOKIE_SECRET valueFrom: secretKeyRef: diff --git a/frontend/helm/ocwa-frontend/templates/downloadDeployment.yaml b/frontend/helm/ocwa-frontend/templates/downloadDeployment.yaml index 1f6bb4354..058e3982a 100644 --- a/frontend/helm/ocwa-frontend/templates/downloadDeployment.yaml +++ b/frontend/helm/ocwa-frontend/templates/downloadDeployment.yaml @@ -54,6 +54,8 @@ spec: value: "download" - name: CODE_EXPORT_ENABLED value: "{{ .Values.codeExportEnabled }}" + - name: REPOSITORY_HOST + value: "{{ .Values.repositoryHost }}" - name: COOKIE_SECRET valueFrom: secretKeyRef: diff --git a/frontend/helm/ocwa-frontend/values.yaml b/frontend/helm/ocwa-frontend/values.yaml index d8fefc2ef..51bded53b 100644 --- a/frontend/helm/ocwa-frontend/values.yaml +++ b/frontend/helm/ocwa-frontend/values.yaml @@ -51,6 +51,7 @@ filesApiHost: "ocwa-storage-api-mongo.ocwa" exporterGroup: "/exporter" ocGroup: "/oc" codeExportEnabled: false +repositoryHost: "repository-url" auth: authorizationEndpoint: "openid.auth.endpoint" callbackURL: "chart-example.local/auth" diff --git a/frontend/server/app.js b/frontend/server/app.js index a7b2379ed..6cfe5ccd4 100644 --- a/frontend/server/app.js +++ b/frontend/server/app.js @@ -33,6 +33,7 @@ const exporterGroup = config.get('exporterGroup'); const ocGroup = config.get('ocGroup'); const exporterMode = config.get('exporterMode'); const codeExportEnabled = config.get('codeExportEnabled'); +const repositoryHost = config.get('repositoryHost'); const memoryStore = new MemoryStore({ checkPeriod: 86400000, // prune expired entries every 24h @@ -106,6 +107,7 @@ app.get('*', checkAuth, storeUrl, (req, res) => { exporterGroup, ocGroup, exporterMode, + repositoryHost, zone: getZone(), }); }); diff --git a/frontend/server/views/index.pug b/frontend/server/views/index.pug index a9f366797..1a5b865e5 100644 --- a/frontend/server/views/index.pug +++ b/frontend/server/views/index.pug @@ -19,6 +19,7 @@ html window.OC_GROUP = !{JSON.stringify(ocGroup)}; window.EXPORTER_MODE = !{JSON.stringify(exporterMode)}; window.CODE_EXPORT_ENABLED = !{JSON.stringify(codeExportEnabled)} + window.REPOSITORY_HOST = !{JSON.stringify(repositoryHost)} window.ZONE = !{JSON.stringify(zone)}; window.HELP_URL = !{JSON.stringify(helpURL)}; diff --git a/frontend/src/components/export-type-icon/index.jsx b/frontend/src/components/export-type-icon/index.jsx index df08dc3e2..d5d3f74a2 100644 --- a/frontend/src/components/export-type-icon/index.jsx +++ b/frontend/src/components/export-type-icon/index.jsx @@ -14,11 +14,12 @@ function ExportTypeIcon({ exportType, large }) { } ExportTypeIcon.propTypes = { - exportType: PropTypes.string.isRequired, + exportType: PropTypes.string, large: PropTypes.bool, }; ExportTypeIcon.defaultProps = { + exportType: '', large: false, }; diff --git a/frontend/src/modules/requests/__tests__/utils.test.js b/frontend/src/modules/requests/__tests__/utils.test.js new file mode 100644 index 000000000..0052827a4 --- /dev/null +++ b/frontend/src/modules/requests/__tests__/utils.test.js @@ -0,0 +1,122 @@ +import { colors } from '@atlaskit/theme'; + +import * as utils from '../utils'; + +describe('request/utils', () => { + describe('phone number validation', () => { + const regex = new RegExp(utils.phoneNumberRegex); + it('should pass an xxx-xxx-xxxx phone number', () => { + expect(regex.test('555-555-5555')).toBeTruthy(); + }); + it('should pass a number only phone number', () => { + expect(regex.test('5555555555')).toBeTruthy(); + }); + it('should fail invalid phone numbers', () => { + expect(regex.test('abc-def-ghij')).toBeFalsy(); + }); + }); + + describe('url validation', () => { + it('should validate a git URL', () => { + const regex = new RegExp(utils.gitUrlRegex); + expect(regex.test('https://github.com/org/repo.git')).toBeTruthy(); + expect(regex.test('asdfasdf')).toBeFalsy(); + }); + + it('should validate a configurable repository URL', () => { + const regex = new RegExp(utils.repositoryRegex); + expect( + regex.test('https://example.com/shares/test/repo.git') + ).toBeTruthy(); + expect( + regex.test('https://my-internal-website.com/test.git') + ).toBeFalsy(); + }); + }); + + describe('request colors', () => { + it('should default to N200 if it is not a state number value', () => { + expect(utils.getRequestStateColor()).toEqual(colors.N200); + expect(utils.getRequestStateColor(null)).toEqual(colors.N200); + expect(utils.getRequestStateColor(undefined)).toEqual(colors.N200); + expect(utils.getRequestStateColor('test')).toEqual(colors.N200); + }); + + it('should pass number states', () => { + expect(utils.getRequestStateColor(0)).toEqual(colors.N200); + expect(utils.getRequestStateColor(1)).toEqual(colors.N200); + expect(utils.getRequestStateColor(2)).toEqual(colors.Y300); + expect(utils.getRequestStateColor(3)).toEqual(colors.Y500); + expect(utils.getRequestStateColor(4)).toEqual(colors.G500); + expect(utils.getRequestStateColor(5)).toEqual(colors.R500); + expect(utils.getRequestStateColor(6)).toEqual(colors.R500); + }); + }); + + describe('request duplication', () => { + it('should duplicate a data request payload', () => { + const original = { + _id: 1, + name: 'Duplicate Me', + phoneNumber: '5555555555', + exportType: 'data', + variableDescriptions: 'Description', + files: ['file1', 'file2'], + supportingFiles: ['sFile1', 'sFile2'], + }; + expect(utils.duplicateRequest(original)).toEqual({ + name: 'Duplicate Me Duplicate', + phoneNumber: '5555555555', + exportType: 'data', + variableDescriptions: 'Description', + files: ['file1', 'file2'], + supportingFiles: ['sFile1', 'sFile2'], + }); + }); + + it('should duplicate a code request payload', () => { + const original = { + _id: 1, + name: 'Duplicate Me', + phoneNumber: '5555555555', + exportType: 'code', + repository: 'http://test.com', + branch: 'develop', + externalRepository: 'http://test.com', + codeDescription: 'Code Description', + }; + expect(utils.duplicateRequest(original)).toEqual({ + name: 'Duplicate Me Duplicate', + phoneNumber: '5555555555', + exportType: 'code', + repository: 'http://test.com', + branch: 'develop', + externalRepository: 'http://test.com', + codeDescription: 'Code Description', + }); + }); + + it('should drop weird or nil values', () => { + const original = { + _id: 1, + name: 'Duplicate Me', + phoneNumber: null, + exportType: 'code', + repository: 'http://test.com', + branch: 'develop', + externalRepository: 'http://test.com', + codeDescription: 'Code Description', + outdatedField: 'asdfasdf', + }; + expect(utils.duplicateRequest(original)).toEqual({ + name: 'Duplicate Me Duplicate', + phoneNumber: '', + exportType: 'code', + repository: 'http://test.com', + branch: 'develop', + externalRepository: 'http://test.com', + codeDescription: 'Code Description', + }); + }); + }); +}); diff --git a/frontend/src/modules/requests/components/request-form/field.jsx b/frontend/src/modules/requests/components/request-form/field.jsx index 6fafcc1c5..936bbcdd9 100644 --- a/frontend/src/modules/requests/components/request-form/field.jsx +++ b/frontend/src/modules/requests/components/request-form/field.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import TextField from '@atlaskit/textfield'; import TextArea from '@atlaskit/textarea'; -import { phoneNumberRegex, urlRegex } from '../../utils'; +import { phoneNumberRegex, repositoryRegex, gitUrlRegex } from '../../utils'; function Field({ type, fieldProps }) { switch (type) { @@ -16,8 +16,13 @@ function Field({ type, fieldProps }) { ); - case 'url': - return ; + case 'repositoryHost': + return ( + + ); + + case 'git': + return ; case 'text': default: @@ -26,7 +31,14 @@ function Field({ type, fieldProps }) { } Field.propTypes = { - type: PropTypes.oneOf(['text', 'tel', 'textarea', 'url']).isRequired, + type: PropTypes.oneOf([ + 'text', + 'tel', + 'textarea', + 'url', + 'git', + 'repositoryHost', + ]).isRequired, fieldProps: PropTypes.object.isRequired, }; diff --git a/frontend/src/modules/requests/components/request/edit-field.jsx b/frontend/src/modules/requests/components/request/edit-field.jsx index f0d05ae30..311264d32 100644 --- a/frontend/src/modules/requests/components/request/edit-field.jsx +++ b/frontend/src/modules/requests/components/request/edit-field.jsx @@ -102,8 +102,14 @@ EditField.propTypes = { data: PropTypes.shape({ key: PropTypes.string.isRequired, name: PropTypes.string.isRequired, - type: PropTypes.oneOf(['text', 'textarea', 'tel', 'email', 'url']) - .isRequired, + type: PropTypes.oneOf([ + 'text', + 'tel', + 'textarea', + 'url', + 'git', + 'repositoryHost', + ]).isRequired, isRequired: PropTypes.bool, value: PropTypes.string.isRequired, }).isRequired, diff --git a/frontend/src/modules/requests/utils.js b/frontend/src/modules/requests/utils.js index 3260a7488..e9d50533f 100644 --- a/frontend/src/modules/requests/utils.js +++ b/frontend/src/modules/requests/utils.js @@ -1,8 +1,10 @@ import { colors } from '@atlaskit/theme'; import flow from 'lodash/flow'; import isNil from 'lodash/isNil'; +import isNumber from 'lodash/isNumber'; import pick from 'lodash/pick'; import mapValues from 'lodash/mapValues'; +import { repositoryHost } from '@src/services/config'; import { _e, getZoneString } from '@src/utils'; // Form content @@ -79,7 +81,7 @@ export const requestFields = [ external: 'Internal repository to send approved results', }), value: 'repository', - type: 'url', + type: 'repositoryHost', exportType: 'code', isRequired: true, helperText: 'Write out the full URL of the repository', @@ -99,7 +101,7 @@ export const requestFields = [ external: 'External repository to send approved results', }), value: 'externalRepository', - type: 'url', + type: 'git', exportType: 'code', isRequired: true, helperText: 'Write out the full URL of the external repository', @@ -108,11 +110,16 @@ export const requestFields = [ // Stored as a string here for native input[type="tel"] elements, so make into // a RegExp if using anywhere else -export const phoneNumberRegex = '[0-9]{3}-[0-9]{3}-[0-9]{4}$'; -export const urlRegex = - '^(?:(?:https?|ftp)://)?(?:(?!(?:10|127)(?:.d{1,3}){3})(?!(?:169.254|192.168)(?:.d{1,3}){2})(?!172.(?:1[6-9]|2d|3[0-1])(?:.d{1,3}){2})(?:[1-9]d?|1dd|2[01]d|22[0-3])(?:.(?:1?d{1,2}|2[0-4]d|25[0-5])){2}(?:.(?:[1-9]d?|1dd|2[0-4]d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:.(?:[a-z\u00a1-\uffff]{2,})))(?::d{2,5})?(?:/S*)?$'; +export const phoneNumberRegex = '[0-9]{3}-?[0-9]{3}-?[0-9]{4}$'; +export const gitUrlRegex = + '((git|ssh|http(s)?)|(git@[w.]+))(:(//)?)([w.@:/-~]+)(.git)(/)?'; +export const repositoryRegex = repositoryHost + ? `${repositoryHost}([w.@:/-~]+)(.git)(/)?` + : gitUrlRegex; export const getRequestStateColor = (value = 0) => { + if (!isNumber(value)) return colors.N200; + switch (value) { case 0: case 1: @@ -125,9 +132,8 @@ export const getRequestStateColor = (value = 0) => { return colors.G500; case 5: case 6: - return colors.R500; default: - return null; + return colors.R500; } }; @@ -163,4 +169,6 @@ export default { getRequestStateColor, requestFields, phoneNumberRegex, + repositoryRegex, + gitUrlRegex, }; diff --git a/frontend/src/services/config.js b/frontend/src/services/config.js index 228f61818..dbe0350a9 100644 --- a/frontend/src/services/config.js +++ b/frontend/src/services/config.js @@ -8,6 +8,7 @@ export const exporterGroup = EXPORTER_GROUP; export const ocGroup = OC_GROUP; export const exporterMode = EXPORTER_MODE; // Can be (undefined || 'export') or 'download' export const codeExportEnabled = CODE_EXPORT_ENABLED; // Can be (undefined || 'export') or 'download' +export const repositoryHost = REPOSITORY_HOST; // Can be (undefined || 'export') or 'download' export const zone = ZONE; export const helpURL = HELP_URL; export const getZone = () => ZONE; @@ -23,6 +24,7 @@ export default { ocGroup, exporterMode, socketHost, + repositoryHost, zone, getZone, }; diff --git a/helm/ocwa/values.yaml b/helm/ocwa/values.yaml index cffd92d48..14d445b24 100644 --- a/helm/ocwa/values.yaml +++ b/helm/ocwa/values.yaml @@ -51,6 +51,7 @@ ocwa-frontend: exporterGroup: "/exporter" ocGroup: "/oc" codeExportEnabled: false + repositoryHost: "repository-host" auth: authorizationEndpoint: "openid.auth.endpoint" callbackURL: "chart-example.local/auth"