diff --git a/.env b/.env index 9e9da271..d1842661 100644 --- a/.env +++ b/.env @@ -1,4 +1,6 @@ REACT_APP_STORE_URL="http://localhost:8010/api/v1/" +# Ensure the trailing / at the end of the STORE_URL + REACT_APP_STORE_USERS_URL="${REACT_APP_STORE_URL}users/" REACT_APP_STORE_AUTH_URL="${REACT_APP_STORE_URL}auth-token/" diff --git a/.eslintrc b/.eslintrc index 2b6b9668..7329dbf6 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,13 +1,42 @@ { - "extends": "react-app", + "extends": [ + "plugin:react/recommended", + "airbnb", + "prettier" + ], "env": { "node": true, "jest": true, "browser": true, "es6": true }, + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parser": "@babel/eslint-parser", + "parserOptions": { + "requireConfigFile": false, + "ecmaVersion": 2018, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + }, + "babelOptions": { + "presets": ["@babel/preset-react"], + "plugins": ["@babel/plugin-proposal-class-properties"] + } + }, + "plugins": [ + "react", + "prettier" + ], "rules": { - "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], - "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js", "./src/setupTests.js"]}] + "react/jsx-filename-extension": [1, { "extensions": [".jsx"] }], + "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js", "./src/setupTests.js"]}], + "react/state-in-constructor": "off", + "react/prop-types": "off", + "react/require-default-props": "off", + "react/jsx-props-no-spreading": "off" } -} +} \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..ea9ebdb2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "printWidth": 100, + "singleQuote": true +} \ No newline at end of file diff --git a/package.json b/package.json index bc4ea01e..7d2c4311 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", - "lint": "eslint ./src/components", - "lint:fix": "eslint --fix ./src/components", + "lint": "eslint src/**/*.{jsx,js}", + "lint:fix": "eslint --fix src/**/*.{jsx,js}", "ci": "concurrently --kill-others-on-fail \"yarn:test\" \"yarn:lint\"", "serve": "sirv build --cors --single --host --port 3000", "deploy": "yarn run build && yarn run serve", @@ -20,47 +20,39 @@ ] }, "dependencies": { + "@babel/eslint-parser": "^7.14.7", + "@babel/preset-react": "^7.14.5", "@fnndsc/chrisstoreapi": "^2.0.2", "@patternfly/patternfly": "^4.59.1", "@patternfly/react-core": "^4.75.2", "@patternfly/react-icons": "^4.7.16", + "@patternfly/react-table": "^4.27.7", "classnames": "^2.2.6", + "concurrently": "^4.1.0", "core-js": "^2.5.7", + "cross-env": "^5.2.0", "dompurify": "^2.2.7", "email-validator": "^2.0.4", + "enzyme": "^3.3.0", + "enzyme-adapter-react-16": "^1.1.1", + "husky": "^0.14.3", + "lint-staged": "^7.2.0", "lodash": "^4.17.21", "marked": "^2.0.3", "moment": "^2.22.2", - "patternfly": "^3.58.0", - "patternfly-react": "^2.24.0", "prop-types": "^15.6.2", - "react": "^16.8.0", + "react": "^17.0.2", "react-bootstrap": "^0.32.1", - "react-clipboard.js": "^2.0.0", - "react-copy-to-clipboard": "^5.0.2", - "react-dom": "^16.8.0", + "react-dom": "^17.0.2", "react-router-dom": "^5.2.0", "react-scripts": "3.4.3", + "react-test-renderer": "^16.4.1", "rxjs": "^6.3.3", "sirv-cli": "^1.0.8", "sortabular": "^1.6.0", "table-resolver": "^4.1.1", "undux": "^5.0.0-beta.18" }, - "devDependencies": { - "concurrently": "^4.1.0", - "cross-env": "^5.2.0", - "enzyme": "^3.3.0", - "enzyme-adapter-react-16": "^1.1.1", - "eslint": "^6.6.0", - "eslint-config-airbnb": "^18.2.0", - "eslint-plugin-import": "^2.22.0", - "eslint-plugin-jsx-a11y": "^6.3.1", - "eslint-plugin-react": "^7.21.2", - "husky": "^0.14.3", - "lint-staged": "^7.2.0", - "react-test-renderer": "^16.4.1" - }, "browserslist": { "production": [ ">0.2%", @@ -72,5 +64,15 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "prettier": "^2.3.2", + "eslint-config-airbnb": "^18.2.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^3.4.0", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-react": "^7.24.0", + "eslint-plugin-react-hooks": "^4.2.0" } } diff --git a/src/components/App/App.css b/src/components/App/App.css index 6271faf6..ae5338d5 100644 --- a/src/components/App/App.css +++ b/src/components/App/App.css @@ -1,6 +1,8 @@ /* CSS variables */ :root { + --pf-global--FontSize--md: 14px !important; + /* PRIMARY COLORS */ /* black */ @@ -48,6 +50,52 @@ /* ---------- GLOBAL CSS -------- */ /* ============================== */ +article { + margin: auto; + margin-bottom: 4em; + max-width: 1200px; +} + +section { + margin: 2em auto; + padding: 1em; +} + +/* Typography */ +h1, h2, h3 { + font-size: 2.5em !important; + margin-top: 0.25em !important; + margin-bottom: 0.5em !important; +} + +h2 { + font-size: 1.75em !important; +} +h1 + h2 { + margin-top: 0; + margin-bottom: 0.5em; +} + +h3 { + font-weight: 500 !important; + font-size: 1.25em !important; +} + +a { + color: inherit; + text-decoration: none; + font-weight: bolder; + position: relative; + + /* Remove blue highlight in mobile */ + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + /* ============================== */ /* ---------- HELPER CSS -------- */ /* ============================== */ @@ -81,4 +129,4 @@ .margin-right-sm { margin-right: 5px; -} +} \ No newline at end of file diff --git a/src/components/App/App.js b/src/components/App/App.jsx similarity index 83% rename from src/components/App/App.js rename to src/components/App/App.jsx index 3e2d66f9..41fcfc85 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.jsx @@ -6,10 +6,10 @@ import ChrisStore from '../../store/ChrisStore'; import './App.css'; // import the patternfly CSS globally -import '../../../node_modules/patternfly/dist/css/patternfly.min.css'; -import '../../../node_modules/patternfly/dist/css/patternfly-additions.min.css'; -import '../../../node_modules/@patternfly/patternfly/patternfly-no-reset.css'; +import '../../../node_modules/@patternfly/patternfly/patternfly.min.css'; +import '../../../node_modules/@patternfly/patternfly/patternfly-base.css'; import '../../../node_modules/@patternfly/patternfly/patternfly-addons.css'; +import '../../../node_modules/@patternfly/patternfly/patternfly-no-reset.css'; /* * The router here serves pages which replace the entire document, @@ -20,7 +20,7 @@ const App = () => (
- + diff --git a/src/components/App/App.test.js b/src/components/App/App.test.jsx similarity index 100% rename from src/components/App/App.test.js rename to src/components/App/App.test.jsx diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.jsx.snap similarity index 100% rename from src/components/App/__snapshots__/App.test.js.snap rename to src/components/App/__snapshots__/App.test.jsx.snap diff --git a/src/components/AppLayout/AppLayout.js b/src/components/AppLayout/AppLayout.jsx similarity index 73% rename from src/components/AppLayout/AppLayout.js rename to src/components/AppLayout/AppLayout.jsx index a9625437..28a3cdf9 100644 --- a/src/components/AppLayout/AppLayout.js +++ b/src/components/AppLayout/AppLayout.jsx @@ -1,16 +1,14 @@ -// based on -// https://github.com/patternfly/patternfly-react-seed/blob/2195cdb69c4a82b64b4cf6870a67750cc1896ef2/src/app/AppLayout/AppLayout.tsx - import React, { useCallback, useState } from 'react'; import { Page, SkipToContent } from '@patternfly/react-core'; +import { useHistory } from 'react-router-dom'; +import Client from '@fnndsc/chrisstoreapi'; import ConnectedNavbar from '../Navbar/Navbar'; import Footer from '../Footer/Footer'; -import './applayout.css' +import './applayout.css'; import Search from '../Navbar/components/Search/Search'; -import { useHistory } from 'react-router-dom'; -import Client from "@fnndsc/chrisstoreapi"; import { debounce } from '../../utils/common'; import ChrisStore from '../../store/ChrisStore'; + const PageSkipToContent = ( Skip to Content @@ -21,34 +19,36 @@ const AppLayout = ({ store, children }) => { const [searchKey, setSearchKey] = useState(''); const [autoCompleteData, setAutoCompleteData] = useState(null); - const auth = { token: store.get("authToken") }; + const auth = { token: store.get('authToken') }; const history = useHistory(); const storeURL = process.env.REACT_APP_STORE_URL; const client = new Client(storeURL, auth); - const debounceSearch = useCallback(debounce(function(value) {onSearch(value)}, 250),[]) - - const onSearch = async (value, MODE='AUTO') => { - const body = { + // eslint-disable-next-line consistent-return + const onSearch = async (value, MODE = 'AUTO') => { + const body = { limit: 20, offset: 0, name_title_category: value, }; try { - if(MODE === 'ENTER') { + if (MODE === 'ENTER') { history.push({ pathname: '/plugins', - search: `q=${value}` + search: `q=${value}`, }); - } else if(MODE === 'AUTO') { + } else if (MODE === 'AUTO') { const searchResults = await client.getPlugins(body); setAutoCompleteData(searchResults.data); + return searchResults.data; } } catch (error) { - console.error(error.message); + return error; } - } + }; + + const debounceSearch = useCallback(debounce((value) => { onSearch(value); }, 250), []); const searchComponent = ( { history={history} onChange={(value) => { setSearchKey(value); - if(value.length >= 3) - debounceSearch(value); + if (value.length >= 3) debounceSearch(value); }} onBlur={() => { - setSearchKey('') + setSearchKey(''); setAutoCompleteData(null); }} onClear={() => setSearchKey('')} onSearch={onSearch} /> - ) + ); return ( } mainContainerId="primary-app-container" skipToContent={PageSkipToContent} additionalGroupedContent={
{searchComponent}
} - > + > {children}
-)}; + ); +}; export default ChrisStore.withStore(AppLayout); diff --git a/src/components/Button/index.js b/src/components/Button/index.jsx similarity index 67% rename from src/components/Button/index.js rename to src/components/Button/index.jsx index efa2122e..84824824 100644 --- a/src/components/Button/index.js +++ b/src/components/Button/index.jsx @@ -1,8 +1,9 @@ -import React from "react"; -import { Button } from "@patternfly/react-core"; -import PropTypes from "prop-types"; -import { useHistory } from "react-router-dom"; -import "./button.css"; +import React from 'react'; +import { Button } from '@patternfly/react-core'; +import PropTypes from 'prop-types'; +import { useHistory } from 'react-router-dom'; +import './button.css'; + const ButtonComponent = ({ variant, onClick, @@ -10,11 +11,11 @@ const ButtonComponent = ({ toRoute, children, type, - ...otherProps }) => { const history = useHistory(); + window.scrollTo(0, 0); return ( -
+
-
-
-
- - { - formError && ( -
-
- } - /> -
-
- ) - } - { - success && ( -
-
- - - Click Here - - {' to view it.'} - -
-
- ) - } -
-
-
- - - - - -
- {formGroups} -
-
-
- - - -
- - {fileText} -
-
-
-
-
-
- -
-
- - - -
-
- - ); - } -} - -export default CreatePlugin; diff --git a/src/components/CreatePlugin/CreatePlugin.jsx b/src/components/CreatePlugin/CreatePlugin.jsx new file mode 100644 index 00000000..a5eb7a9e --- /dev/null +++ b/src/components/CreatePlugin/CreatePlugin.jsx @@ -0,0 +1,437 @@ +import React, { Component } from 'react'; +import Client from '@fnndsc/chrisstoreapi'; +import { Link } from 'react-router-dom'; +import { + CardHeader, + Grid, + GridItem, + Form, + FormGroup, + TextInput, + FileUpload, + Alert, + Card, + CardBody, + CodeBlock, + CodeBlockCode, +} from '@patternfly/react-core'; +import { FileIcon, UploadIcon, ExclamationTriangleIcon } from '@patternfly/react-icons'; + +import Button from '../Button'; +import './CreatePlugin.css'; + +class CreatePlugin extends Component { + constructor() { + super(); + + this.state = { + dragOver: false, + isNameValidated: 'default', + name: String(), + image: String(), + repo: String(), + pluginRepresentation: Object(), + fileError: false, + formError: null, + success: false, + }; + + this.storeURL = process.env.REACT_APP_STORE_URL; + + this.formGroupsData = [ + { + id: 'name', + label: 'Plugin Name', + help: 'Choose a unique name', + validated: 'isNameValidated', + validation: async (value) => { + if (value.length < 5) return 'warning'; + + try { + const result = await this.Client().getPluginMetas({ + name_title_category: value, + }); + if (result.data.length === 0) return 'success'; + return 'error'; + } catch (error) { + return 'warning'; + } + }, + }, + { + id: 'image', + label: 'Docker Image', + help: 'DockerHub URL of the image to be used', + }, + { + id: 'repo', + label: 'Repository', + help: 'Public URL to your source code', + }, + ]; + + [ + 'setFileError', 'handleError', 'hideError', 'handleChange', + 'handleFile', 'readFile', 'handleSubmit', + ].forEach((method) => { + this[method] = this[method].bind(this); + }); + } + + handleError(message) { + let errorObj; + try { + errorObj = JSON.parse(message); + if (Object.prototype.hasOwnProperty.call(errorObj, 'non_field_errors')) { + this.setState({ + formError: errorObj.non_field_errors, + }); + } else if (Object.prototype.hasOwnProperty.call(errorObj, 'public_repo')) { + this.setState({ + formError: errorObj.public_repo, + }); + } else { + this.setState({ formError: message }); + } + } catch (e) { + this.setState({ formError: message }); + } + } + + handleChange(value, { target }, check) { + const nextState = { [target.name]: value }; + this.setState(nextState); + + if (check) { + check.validation(value).then((status) => { + this.setState({ + [check.validated]: status, + }); + }); + } + } + + handleFile(value, filename) { + if (value && value.type === 'application/json') { + this.setState({ fileName: filename }); + this.readFile(value) + .then(() => this.setFileError(false)) + .catch(() => this.setFileError(true)); + } else { + this.setState({ + fileName: undefined, + pluginRepresentation: {}, + }); + this.setFileError(true); + } + } + + async handleSubmit() { + const { + name: pluginName, + image: pluginImage, + repo: pluginRepo, + pluginRepresentation, + } = this.state; + + // Array to store the errors + const errors = []; + let missingRepresentationString = ''; + const inputImage = pluginImage.trim(); + if (inputImage) { + if (inputImage.endsWith(':latest')) { + this.handleError( + + The + :latest + tag is discouraged. + , + ); + } else if (!inputImage.includes(':')) { + /** + * @todo + * We can provide specific feedback based on the plugin's JSON description, + * if it is uploaded. + */ + const tag = inputImage.split(':')[0]; + this.handleError( +
+

+ Please tag your Docker image by version. +
+ Example: +

+ + + docker tag + {tag} + {tag} + :1.0.1 +
+ docker push + {tag} + :1.0.1 +
+
+
, + ); + } + } + + if (!( + pluginName.trim() && pluginImage.trim() && + pluginRepo.trim() && pluginRepresentation && + Object.keys(pluginRepresentation).length > 0 + )) { + // Checks for individual field completion + if (!pluginName.trim()) { + errors.push('Plugin Name'); + } + if (!pluginImage.trim()) { + errors.push('Docker Image'); + } + if (!pluginRepo.trim()) { + errors.push('Public Repo'); + } + if (!Object.keys(pluginRepresentation).length > 0) { + missingRepresentationString = 'upload the plugin representation and '; + } + + // If all the fields are empty in submission + if (errors.length === 3 && missingRepresentationString !== '') { + return this.handleError('All fields are required.'); + // If one fields is empty + } if (errors.length === 1) { + return this.handleError(`Please ${missingRepresentationString}enter + the ${errors[0]}`); + // If two fields are empty + } if (errors.length === 2) { + return this.handleError(`Please ${missingRepresentationString}enter + the ${errors[0]} and ${errors[1]}`); + // If more than Plugin Representation or two fields are empty or + } + // If only the Plugin Representation is missing + if (errors.length === 0) { + return this.handleError('Please upload the plugin representation'); + } + const lastError = errors.pop(); + return this.handleError(`Please ${missingRepresentationString}enter + the ${errors.join(', ')}, and ${lastError}`); + } + + const fileData = JSON.stringify(pluginRepresentation); + const file = new Blob([fileData], { type: 'application/json' }); + const fileObj = { descriptor_file: file }; + + const pluginData = { + name: pluginName, + dock_image: pluginImage, + public_repo: pluginRepo, + }; + + const client = this.Client(); + + let newPlugin; + try { + const resp = await client.createPlugin(pluginData, fileObj); + newPlugin = resp.data; + } catch ({ message }) { + return this.handleError(message); + } + + this.hideError(); + this.setState({ success: true, newPlugin }); + return newPlugin; + } + + setFileError(state) { + this.setState((prevState) => { + const nextState = {}; + if (typeof state !== 'undefined') { + nextState.fileError = state; + } else { + nextState.fileError = !prevState.fileError; + } + return nextState; + }); + } + + readFile(file) { + return new Promise((resolve, reject) => { + const invalidRepresentation = new Error('Invalid Plugin Representation'); + if (file instanceof Blob) { + const reader = new FileReader(); + reader.onload = (e) => { + const { target: { result } } = e; + + let pluginRepresentation; + try { + pluginRepresentation = JSON.parse(result); + } catch (_err) { + reject(invalidRepresentation); + } + + this.setState({ pluginRepresentation, fileError: false }, resolve); + }; + + reader.onerror = () => reject(invalidRepresentation); + reader.readAsText(file); + } else { + reject(invalidRepresentation); + } + }); + } + + Client() { + const token = window.sessionStorage.getItem('AUTH_TOKEN'); + return new Client(this.storeURL, { token }); + } + + hideError() { + this.setState({ formError: null }); + } + + render() { + const { + fileName, fileError, formError, success, newPlugin, + } = this.state; + + let pluginId; + if (newPlugin) { + pluginId = newPlugin.id; + } + + // generate formGroups based on data + const PluginFormDataGroups = this.formGroupsData.map( + ({ id, label, help, validated, validation }) => ( + + this.handleChange(...args, { validated, validation }) + )} + placeholder={help} + isRequired + /> + + ) + ); + + return ( +
+ + + + +

Create Plugin


+
+ + +

Plugin Creation

+

+ Plugins should already exist and have their own public source repo and existing docker image. + Adding a plugin to the store simply adds the location of your plugin, allowing other users to access it. +

+
+ + +

About Plugins

+

+ Plugins should already exist and have their own public source repo and existing docker image. + Adding a plugin to the store simply adds the location of your plugin, allowing other users to access it. +

+
+
+
+ + + + { + formError ? ( + + + + ) : null + } + + { + success ? ( + + + + Click Here to view. + + + + ) : null + } + + + + +

Add Plugin Details

+
+ + +
+ { PluginFormDataGroups } + + +
+ + { // eslint-disable-next-line no-nested-ternary + fileError ? : ( + !fileName ? : + )} + + +
+
+ + +
+
+
+
+
+
+
+
+ ); + } +} + +export default CreatePlugin; diff --git a/src/components/CreatePlugin/CreatePlugin.test.js b/src/components/CreatePlugin/CreatePlugin.test.jsx similarity index 96% rename from src/components/CreatePlugin/CreatePlugin.test.js rename to src/components/CreatePlugin/CreatePlugin.test.jsx index 1734e4ea..71ac4107 100644 --- a/src/components/CreatePlugin/CreatePlugin.test.js +++ b/src/components/CreatePlugin/CreatePlugin.test.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { ControlLabel } from 'patternfly-react'; +import { Label as ControlLabel } from '@patternfly/react-core'; import CreatePlugin from './CreatePlugin'; import validPluginRepresentation from './samplePluginRepresentation'; @@ -175,7 +175,7 @@ describe('CreatePlugin', () => { }); }); - const getFormGroupFromId = id => wrapper + const getFormGroupFromId = (id) => wrapper .find('Form.createplugin-form') .find(`FormGroup[controlId="${id}"]`); @@ -233,7 +233,7 @@ describe('CreatePlugin', () => { /* ============ UPLOAD ========== */ /* ============================== */ - const findUploadFormGroup = shallowWrapper => shallowWrapper + const findUploadFormGroup = (shallowWrapper) => shallowWrapper .find('Form.createplugin-form') .find('FormGroup[controlId="file"]'); @@ -264,7 +264,7 @@ describe('CreatePlugin', () => { }); const defaultEventProps = { - preventDefault: jest.fn(), + persist: jest.fn(), stopPropagation: jest.fn(), }; @@ -285,7 +285,7 @@ describe('CreatePlugin', () => { it('should prevent event when handleDrag is called', () => { const event = { type: 'dragstart', ...defaultEventProps }; wrapper.instance().handleDrag(event); - expect(event.preventDefault).toHaveBeenCalled(); + expect(event.persist).toHaveBeenCalled(); expect(event.stopPropagation).toHaveBeenCalled(); }); @@ -519,7 +519,12 @@ describe('CreatePlugin', () => { ...formState, }); await mockedWrapper.instance().handleSubmit(); - expect(spy).toHaveBeenCalledWith(The :latest tag is discouraged.); + expect(spy).toHaveBeenCalledWith( + The + :latest + {' '} + tag is discouraged. + ); }); it('should call handleError if no docker tag is provided', async () => { @@ -534,10 +539,20 @@ describe('CreatePlugin', () => { }); await mockedWrapper.instance().handleSubmit(); const description = 'Please tag your Docker image by version.'; - const tagExample = `docker tag ${formState.image.split(':')[0]} ${formState.image.split(':')[0]}:1.0.1`; //TODO + const tagExample = `docker tag ${formState.image.split(':')[0]} ${formState.image.split(':')[0]}:1.0.1`; // TODO const pushExample = `docker push ${formState.image.split(':')[0]}:1.0.1`; - expect(spy).toHaveBeenCalledWith(
{description}

Example:
{tagExample}
{pushExample}

) - }) + expect(spy).toHaveBeenCalledWith(
+ {description} + {' '} +

+ Example: +
+ {tagExample} +
+ {pushExample} +

+
); + }); it('should call handleError if all fields are filled but invalid JSON is received', async () => { window.sessionStorage.setItem('AUTH_TOKEN', 'testToken'); diff --git a/src/components/CreatePlugin/constant.js b/src/components/CreatePlugin/constant.js deleted file mode 100644 index 32bf23a5..00000000 --- a/src/components/CreatePlugin/constant.js +++ /dev/null @@ -1 +0,0 @@ -export const createPluginHint = "Plugins should already exist and have their own public source repo and existing docker image. Adding a plugin to the store simply adds the location of your plugin, as well as some metadata, to the store, allowing other users easy access to it." \ No newline at end of file diff --git a/src/components/Dashboard/Dashboard.css b/src/components/Dashboard/Dashboard.css index e7962910..f95e4af1 100644 --- a/src/components/Dashboard/Dashboard.css +++ b/src/components/Dashboard/Dashboard.css @@ -1,67 +1,35 @@ -.title-bar { - /* color */ +@media (min-width: 768px) { + #dashboard-container { + max-width: 1200px; + } +} + +#title-bar { + margin-top: 1em; + padding: 1em 2em; +} +#title-name { color: #393f44; color: var(--pf-black-800); - - /* display */ - padding: 1em; - - /* font */ font-size: 1.5em; font-weight: 600; } - -.dashboard-row { - display: flex; - flex-direction: row; - flex-wrap: wrap; - width: 100%; - padding-top: 20px; -} - -.dashboard-spinner{ - margin: 0 auto; -} - -.dashboard-left-column { +#title-actions-container { display: flex; - flex-direction: column; - flex: 3; - gap: 10px; - margin-bottom: 10px; + flex-direction: row-reverse; +} #title-actions-container > div { + margin-left: 1em; } -.dashboard-right-column { +#dashboard-spinner { display: flex; - flex-direction: column; - flex: 1; + flex-direction: column; + padding: 4em; +} #dashboard-spinner > * { + margin: auto; } -.dashboard__developercta-button { - height: 50px; - width: 100%; +#dashboard-body { + margin: 2em; } -.dashboard-body { - margin-top: -20px; -} - -.dashboard-container { - margin: 0 auto; - margin-top: 20px; -} -@media (min-width: 768px){ - .dashboard-container{ - width: 760px; - } -} -@media (min-width: 992px){ - .dashboard-container{ - width: 980px; - } -} -@media (min-width: 1200px){ - .dashboard-container{ - width: 1180px; - } -} \ No newline at end of file diff --git a/src/components/Dashboard/Dashboard.js b/src/components/Dashboard/Dashboard.jsx similarity index 61% rename from src/components/Dashboard/Dashboard.js rename to src/components/Dashboard/Dashboard.jsx index 50ed82dd..8efe12df 100644 --- a/src/components/Dashboard/Dashboard.js +++ b/src/components/Dashboard/Dashboard.jsx @@ -1,25 +1,26 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; -import Button from "../Button"; import Client from "@fnndsc/chrisstoreapi"; +import { Spinner, Grid, GridItem, Split, SplitItem } from "@patternfly/react-core"; + import "./Dashboard.css"; + +import Button from "../Button"; import DashPluginCardView from "./components/DashPluginCardView/DashPluginCardView"; import DashTeamView from "./components/DashTeamView/DashTeamView"; import DashGitHubView from "./components/DashGitHubView/DashGitHubView"; import ChrisStore from "../../store/ChrisStore"; -import Notification from "../Notification"; +import ErrorNotification from "../Notification"; import HttpApiCallError from "../../errors/HttpApiCallError"; -import { Spinner } from "@patternfly/react-core"; class Dashboard extends Component { constructor(props) { super(props); this.state = { - pluginList: null, + pluginList: [], loading: true, error: null, }; - this.initialize = this.initialize.bind(this); this.deletePlugin = this.deletePlugin.bind(this); this.editPlugin = this.editPlugin.bind(this); } @@ -27,14 +28,9 @@ class Dashboard extends Component { componentDidMount() { this.fetchPlugins().catch((err) => { this.showNotifications(new HttpApiCallError(err)); - console.error(err); }); } - showNotifications = (error) => { - this.setState({ - error: error.message, - }); - }; + fetchPlugins() { const { store } = this.props; const storeURL = process.env.REACT_APP_STORE_URL; @@ -44,14 +40,13 @@ class Dashboard extends Component { limit: 20, offset: 0, }; - this.setState({ loading: true, pluginList: null }); - return client.getPlugins(searchParams).then((plugins) => { - this.setState((prevState) => { - const prevPluginList = prevState.pluginList ? prevState.pluginList : []; - const nextPluginList = prevPluginList.concat(plugins.data); - return { pluginList: nextPluginList, loading: false }; - }); + return client.getPluginMetas(searchParams).then((plugins) => { + this.setState(({ pluginList }) => ({ + pluginList: pluginList.concat(plugins.data), + loading: false + })); + return plugins.data; }); } @@ -98,23 +93,20 @@ class Dashboard extends Component { } return response; } - - initialize() { - const { arePluginsAvailable } = this.state; - + + showNotifications(error) { this.setState({ - arePluginsAvailable: !arePluginsAvailable, + error: error.message, }); - } + }; render() { const { pluginList, loading, error } = this.state; - const { store } = this.props; - const userName = store.get("userName") || ""; + return ( - + <> {error && ( - this.setState({ error: null })} /> )} -
-
-
{`Dashboard for ${userName}`}
-
+ +
+ + +

Dashboard

+
+ + -
-
-
-
-
-
- {loading ? ( -
- + {/* */} + + + + { + loading ? ( +
+
) : ( - -
- - -
-
+ + + + + + + + + + + + + + -
-
- )} -
-
-
- + + + ) + } + + ); } } + Dashboard.propTypes = { store: PropTypes.objectOf(PropTypes.object), }; diff --git a/src/components/Dashboard/components/DashGitHubView/DashGitHubView.css b/src/components/Dashboard/components/DashGitHubView/DashGitHubView.css index 1802753c..7c253287 100644 --- a/src/components/Dashboard/components/DashGitHubView/DashGitHubView.css +++ b/src/components/Dashboard/components/DashGitHubView/DashGitHubView.css @@ -29,8 +29,6 @@ padding-left: 5px; } -.github-plugin-noplugin-text { - padding-top: 10px; - padding-left: 5px; - font-size: 1.2em; +.pf-c-card__title { + font-size: larger; } diff --git a/src/components/Dashboard/components/DashGitHubView/DashGitHubView.js b/src/components/Dashboard/components/DashGitHubView/DashGitHubView.js deleted file mode 100644 index 8ff03083..00000000 --- a/src/components/Dashboard/components/DashGitHubView/DashGitHubView.js +++ /dev/null @@ -1,59 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import isEmpty from 'lodash/isEmpty'; -import { - ListView, - Col, -} from 'patternfly-react'; -import {Card, CardTitle, CardBody} from '@patternfly/react-core'; -import './DashGitHubView.css'; -import BrainyPointer from '../../../../assets/img/brainy-pointer.png'; - -const DashGitHubEmptyState = () => ( -
- - Revisions Panel -

The most recent 10 changes to your plugins will appear here.

-
- Click Add Plugin -
-
); - -class DashGitHubView extends Component { - constructor(props) { - super(props); - this.state = { - }; - } - - render() { - const { plugins } = this.props; - const showEmptyState = isEmpty(plugins); - - return ( - - - - Revisions to My Plugins - - - { showEmptyState ? - - : - - } - - - - ); - } -} -DashGitHubView.propTypes = { - plugins: PropTypes.arrayOf(PropTypes.object), -}; - -DashGitHubView.defaultProps = { - plugins: [], -}; - -export default DashGitHubView; diff --git a/src/components/Dashboard/components/DashGitHubView/DashGitHubView.jsx b/src/components/Dashboard/components/DashGitHubView/DashGitHubView.jsx new file mode 100644 index 00000000..1a874213 --- /dev/null +++ b/src/components/Dashboard/components/DashGitHubView/DashGitHubView.jsx @@ -0,0 +1,52 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import isEmpty from 'lodash/isEmpty'; +import { + List, Card, CardTitle, CardBody, +} from '@patternfly/react-core'; + +import './DashGitHubView.css'; +import BrainyPointer from '../../../../assets/img/brainy-pointer.png'; + +const DashGitHubEmptyState = () => ( +
+

The most recent 10 changes to your plugins will appear here.

+ Click to Add Plugin +
+); + +class DashGitHubView extends Component { + constructor(props) { + super(props); + this.state = { + }; + } + + render() { + const { plugins } = this.props; + const showEmptyState = isEmpty(plugins); + + return ( + + + Revisions + + + { showEmptyState + ? + : } + + + ); + } +} + +DashGitHubView.propTypes = { + plugins: PropTypes.arrayOf(PropTypes.object), +}; + +DashGitHubView.defaultProps = { + plugins: [], +}; + +export default DashGitHubView; diff --git a/src/components/Dashboard/components/DashPluginCardView/DashPluginCardView.css b/src/components/Dashboard/components/DashPluginCardView/DashPluginCardView.css index 55d5a6b1..ad8bd101 100644 --- a/src/components/Dashboard/components/DashPluginCardView/DashPluginCardView.css +++ b/src/components/Dashboard/components/DashPluginCardView/DashPluginCardView.css @@ -1,47 +1,3 @@ -@media (min-width: 1px) { - .card-body-content-parent { - flex-wrap: wrap; - } -} -@media (min-width: 598px) { - .card-body-content-parent { - flex-wrap: wrap; - } -} -@media (min-width: 768px) { - .card-pf-body .blank-slate-pf { - padding: 70px; - margin: -20px; - } - .card-body-content-parent { - flex-wrap: wrap; - } -} -@media (min-width: 992px) { - .card-pf-body .blank-slate-pf { - padding: 70px; - padding-bottom: 65px; - margin: -20px; - } - .card-body-content-parent { - flex-wrap: nowrap; - } -} - -@media (min-width: 1200px) { - .card-pf-body .blank-slate-pf { - margin: -20px; - } - .card-body-content-parent { - flex-wrap: nowrap; - } -} - -.card-pf-body .blank-slate-pf { - background-color: #fff; - border: none; -} - .card-body-empty { padding: 10px !important; background: linear-gradient( @@ -84,20 +40,7 @@ padding: 33px !important; } -.card-view-title .pf-c-dropdown { - padding: 0; - margin-left: 20px; - -} -.card-view-title .pf-c-dropdown .pf-m-plain{ - outline: none; -} -.card-view-title .pf-c-dropdown .pf-m-plain:hover{ - color: #0088ce; -} -.card-view-kebob .pf-c-dropdown__menu { - list-style-type: none; -} + .card-view-title{ display: flex; @@ -120,8 +63,8 @@ /* display */ display: inline-block; - margin-right: 0.5em; - padding: 0 0.25em; + margin: 0 0.5em; + padding: 0 0.5em; /* border */ border-color: #8b8d8f; @@ -130,16 +73,23 @@ /* font */ font-weight: 600; - margin-bottom: 10px; font-size: 12px; } -.card-view-tag-title { - display: inline-block; - margin-bottom: -10px; -} - .card-view-app-type { font-size: 14px; - padding: 0 3px 10px 3px; } + +.card-info-points { + margin: 0.5em 0; + font-size: 0.8em; + font-weight: bold; + color: gray; +} + +.pf-c-card__title { + font-size: larger; +} +.pf-c-dropdown__toggle { + padding-right: 0 !important; +} \ No newline at end of file diff --git a/src/components/Dashboard/components/DashPluginCardView/DashPluginCardView.js b/src/components/Dashboard/components/DashPluginCardView/DashPluginCardView.js deleted file mode 100644 index e0a820f3..00000000 --- a/src/components/Dashboard/components/DashPluginCardView/DashPluginCardView.js +++ /dev/null @@ -1,294 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { Link } from "react-router-dom"; -import isEmpty from "lodash/isEmpty"; -import { - Col, - EmptyStateAction, - EmptyStateInfo, - FieldLevelHelp, - MessageDialog -} from "patternfly-react"; -import { CardTitle, CardBody, Card, DropdownItem, Dropdown, KebabToggle, GridItem, Grid, Form } from "@patternfly/react-core"; -import Button from "../../../Button"; -import "./DashPluginCardView.css"; -import BrainImg from "../../../../assets/img/empty-brain-xs.png"; -import PluginPointer from "../../../../assets/img/brainy_welcome-pointer.png"; -import RelativeDate from "../../../RelativeDate/RelativeDate"; -import FormInput from "../../../FormInput"; - -const DashGitHubEmptyState = () => ( - - - My Plugins - -

- You have no plugins in the ChRIS store -

-

Lets fix that!

-
-
- Click Add Plugin -
-
-

- Create a new listing for your plugin in the ChRIS store by - clicking "Add Plugin" below. -

- -
-
-
-
- -); - -const DashApplicationType = type => { - if (type === "ds") { - return ( - - Data System - - ); - } - return ( - - File System - - ); -}; - -class DashPluginCardView extends Component { - constructor(props) { - super(props); - - this.state = { - showDeleteConfirmation: false, - showEditConfirmation: false, - pluginToDelete: null, - pluginToEdit: null, - publicRepo: "", - isOpen: [], - }; - - const methods = [ - "deletePlugin", - "secondaryDeleteAction", - "showDeleteModal", - "editPlugin", - "secondaryEditAction", - "showEditModal", - "handlePublicRepo" - ]; - methods.forEach(method => { - this[method] = this[method].bind(this); - }); - } - deletePlugin() { - const { onDelete } = this.props; - const { pluginToDelete } = this.state; - onDelete(pluginToDelete.id); - this.setState({ showDeleteConfirmation: false }); - } - secondaryDeleteAction() { - this.setState({ showDeleteConfirmation: false }); - } - showDeleteModal(plugin) { - this.setState({ - showDeleteConfirmation: true, - pluginToDelete: plugin - }); - } - editPlugin() { - const { onEdit } = this.props; - const { pluginToEdit } = this.state; - onEdit(pluginToEdit.id, this.state.publicRepo); - this.setState({ showEditConfirmation: false }); - } - secondaryEditAction() { - this.setState({ showEditConfirmation: false }); - } - showEditModal(plugin) { - this.setState({ - showEditConfirmation: true, - pluginToEdit: plugin - }); - } - handlePublicRepo(value) { - this.setState({ publicRepo: value }); - } - toggle = (value, id) => { - const pluginLength = this.props.plugins.length; - let isOpen = new Array(pluginLength); - isOpen[id] = value; - this.setState({ - isOpen: [...isOpen], - }); - } - onSelect = (event, plugin) => { - const actionType = event.target.innerText; - if (actionType.includes('Edit')) { - this.showEditModal(plugin); - } else if (actionType.includes('Delete')) { - this.showDeleteModal(plugin); - } - } - render() { - let pluginCardBody; - const { plugins } = this.props; - const { - pluginToDelete, - showDeleteConfirmation, - pluginToEdit, - showEditConfirmation - } = this.state; - const showEmptyState = isEmpty(plugins); - const primaryDeleteContent =

Are you sure?

; - const secondaryDeleteContent = ( -

- Plugin {pluginToDelete ? pluginToDelete.name : null} will be - permanently deleted -

- ); - const secondaryEditContent = pluginToEdit ? ( - - -
- this.handlePublicRepo(value)} - fieldName="publicRepo" - helperText="Enter the public repo URL for your plugin" - /> - -
-
- ) : null; - const addNewPlugin = ( - - - -
- Add new plugin -
- - Click below to add a new ChRIS plugin - - - - -
-
-
- ); - if (plugins) { - pluginCardBody = plugins.map((plugin, id) => { - const creationDate = new RelativeDate(plugin.creation_date); - const applicationType = DashApplicationType(plugin.type); - return ( - - - -
- - {plugin.name} - -
- {plugin.description}
} /> -
-
- this.onSelect(event, plugin)} - toggle={ this.toggle(value, id)} id={`kebab-${plugin.id}`}/>} - isOpen={this.state.isOpen[id]} - isPlain - dropdownItems={[ - Edit, - Delete - ]} - /> - - - -
{applicationType}
-
-
- {`Version ${plugin.version}`} -
-
-
-
- {creationDate.isValid() && - `Created ${creationDate.format()}`} -
-
-
-
- {`${plugin.license} license`} -
-
-
- - - ); - }); - pluginCardBody.push(addNewPlugin); - } - return showEmptyState ? ( - - ) : ( - -
- - {pluginCardBody} - - - -
-
- ); - } -} - -DashPluginCardView.propTypes = { - plugins: PropTypes.arrayOf(PropTypes.object), - onDelete: PropTypes.func.isRequired, - onEdit: PropTypes.func.isRequired -}; - -DashPluginCardView.defaultProps = { - plugins: [] -}; - -export default DashPluginCardView; diff --git a/src/components/Dashboard/components/DashPluginCardView/DashPluginCardView.jsx b/src/components/Dashboard/components/DashPluginCardView/DashPluginCardView.jsx new file mode 100644 index 00000000..8829a4ec --- /dev/null +++ b/src/components/Dashboard/components/DashPluginCardView/DashPluginCardView.jsx @@ -0,0 +1,288 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { Link } from "react-router-dom"; +import isEmpty from "lodash/isEmpty"; +import { + Card, + CardHeader, + CardTitle, + CardBody, + CardActions, + DropdownItem, + Dropdown, + KebabToggle, + Grid, + GridItem, + Form, + Modal +} from "@patternfly/react-core"; + +import Button from "../../../Button"; +import "./DashPluginCardView.css"; +import BrainImg from "../../../../assets/img/empty-brain-xs.png"; +import PluginPointer from "../../../../assets/img/brainy_welcome-pointer.png"; +import RelativeDate from "../../../RelativeDate/RelativeDate"; +import FormInput from "../../../FormInput"; + +const DashGitHubEmptyState = () => ( + + My Plugins + +
+
+ Click Add Plugin +
+
+

+ Create a new listing for your plugin in the ChRIS store by + clicking "Add Plugin" below. +

+ +
+
+
+
+); + +const DashApplicationType = type => { + if (type === "ds") { + return ( + <> + {" "} + Data System + + ); + } + return ( + <> + {" "} + File System + + ); +}; + +class DashPluginCardView extends Component { + constructor(props) { + super(props); + + this.state = { + showDeleteConfirmation: false, + showEditConfirmation: false, + pluginToDelete: null, + pluginToEdit: null, + publicRepo: "", + isOpen: [], + }; + } + + editPlugin = () => { + const { onEdit } = this.props; + const { pluginToEdit, publicRepo } = this.state; + onEdit(pluginToEdit.id, publicRepo); + this.setState({ showEditConfirmation: false }); + } + + deletePlugin = () => { + const { onDelete } = this.props; + const { pluginToDelete } = this.state; + onDelete(pluginToDelete.id); + this.setState({ showDeleteConfirmation: false }); + } + + showDeleteModal = (plugin) => { + this.setState({ + showDeleteConfirmation: true, + pluginToDelete: plugin + }); + } + + showEditModal = (plugin) => { + this.setState({ + showEditConfirmation: true, + pluginToEdit: plugin + }); + } + + handlePublicRepo = (value) => { + this.setState({ publicRepo: value }); + } + + toggleEditMenu = (value, cardidx) => { + const { plugins } = this.props; + const isOpen = new Array(plugins.length); + isOpen[cardidx] = value; + this.setState({ isOpen }); + } + + onSelect = (event, plugin) => { + const actionType = event.target.innerText; + if (actionType.includes('Edit')) { + this.showEditModal(plugin); + } else if (actionType.includes('Delete')) { + this.showDeleteModal(plugin); + } + } + + render() { + const { plugins } = this.props; + const { + pluginToDelete, + pluginToEdit, + showDeleteConfirmation, + showEditConfirmation, + isOpen, + } = this.state; + + const showEmptyState = isEmpty(plugins); + + const pluginCardBody = plugins.map((plugin, index) => { + const creationDate = new RelativeDate(plugin.creation_date); + const applicationType = DashApplicationType(plugin.type); + return ( + + + + + {plugin.name} + + + this.onSelect(event, plugin)} + toggle={ this.toggleEditMenu(value, index)} id={`kebab-${plugin.id}`}/>} + isOpen={isOpen[index]} + isPlain + dropdownItems={[ + Edit, + Delete + ]} + /> + + + + +

{plugin.title}

+
+

{creationDate.isValid() && `Created ${creationDate.format()}`}

+

{`${plugin.license} license`}

+
+ +
{applicationType}
+
+
+
+ ); + }) + + const secondaryEditContent = pluginToEdit ? ( + + +
+ this.handlePublicRepo(value)} + fieldName="publicRepo" + helperText="Enter the public repo URL for your plugin" + /> + +
+
+ ) : null; + + const closeEditModal = () => { + this.setState({ showEditConfirmation: false, pluginToEdit: null }) + } + + const secondaryDeleteContent = ( +

+ Plugin {pluginToDelete ? pluginToDelete.name : null} will be + permanently deleted +

+ ); + + const closeDeleteModal = () => { + this.setState({ showDeleteConfirmation: false, pluginToDelete: null }) + } + + const addNewPlugin = ( + + + +
+ Add new plugin +
+

Click below to add a new ChRIS plugin

+
+ +
+
+
+
+ ); + + return showEmptyState ? ( + + ) : ( + <> +
+ + {pluginCardBody} + {addNewPlugin} + + + + Confirm + , + + ]} + > + {secondaryEditContent} + + + + Delete + , + + ]} + > + {secondaryDeleteContent} + +
+ + ); + } +} + +DashPluginCardView.propTypes = { + plugins: PropTypes.arrayOf(PropTypes.object), + onDelete: PropTypes.func.isRequired, + onEdit: PropTypes.func.isRequired +}; + +DashPluginCardView.defaultProps = { + plugins: [] +}; + +export default DashPluginCardView; diff --git a/src/components/Dashboard/components/DashTeamView/DashTeamView.css b/src/components/Dashboard/components/DashTeamView/DashTeamView.css index 8b03b5f0..ef000624 100644 --- a/src/components/Dashboard/components/DashTeamView/DashTeamView.css +++ b/src/components/Dashboard/components/DashTeamView/DashTeamView.css @@ -1,11 +1,11 @@ -.github-team-noplugin-title { - padding-left: 5px; -} - .dash-text-div { padding-top: 6px; } .card-footer{ color: #0088ce; +} + +.pf-c-card__title { + font-size: larger; } \ No newline at end of file diff --git a/src/components/Dashboard/components/DashTeamView/DashTeamView.js b/src/components/Dashboard/components/DashTeamView/DashTeamView.js deleted file mode 100644 index 2da7b3c0..00000000 --- a/src/components/Dashboard/components/DashTeamView/DashTeamView.js +++ /dev/null @@ -1,262 +0,0 @@ -import React, { Component } from "react"; -import isEmpty from "lodash/isEmpty"; -import orderBy from "lodash/orderBy"; -import PropTypes from "prop-types"; -import * as sort from "sortabular"; -import * as resolve from "table-resolver"; -import { - actionHeaderCellFormatter, - customHeaderFormattersDefinition, - defaultSortingOrder, - sortableHeaderCellFormatter, - tableCellFormatter, - Table, - TABLE_SORT_DIRECTION, - MenuItem, - Col, - Icon -} from "patternfly-react"; -import { CardTitle, CardBody, Card, CardFooter, Grid, GridItem, CardActions } from "@patternfly/react-core"; -import BrainyTeammatesPointer from "../../../../assets/img/brainy_teammates-pointer.png"; -import "./DashTeamView.css"; - -const DashTeamEmptyState = () => ( -
-
- -
-
- - Teammates Panel - -

- In this area, you will be able to add and manage teammates to help you - with each plugin. -

-
-
- Click Add Plugin -
-
-); - -class DashTeamView extends Component { - constructor(props) { - super(props); - // Point the transform to your sortingColumns. React state can work for this purpose - // but you can use a state manager as well. - const getSortingColumns = () => this.state.sortingColumns || {}; - - const sortableTransform = sort.sort({ - getSortingColumns, - onSort: selectedColumn => { - this.setState({ - sortingColumns: sort.byColumn({ - sortingColumns: this.state.sortingColumns, - sortingOrder: defaultSortingOrder, - selectedColumn - }) - }); - }, - // Use property or index dependening on the sortingColumns structure specified - strategy: sort.strategies.byProperty - }); - - const sortingFormatter = sort.header({ - sortableTransform, - getSortingColumns, - strategy: sort.strategies.byProperty - }); - - // enables our custom header formatters extensions to reactabular - this.customHeaderFormatters = customHeaderFormattersDefinition; - - this.state = { - // Sort the first column in an ascending way by default. - sortingColumns: { - name: { - direction: TABLE_SORT_DIRECTION.ASC, - position: 0 - } - }, - columns: [ - { - property: "name", - header: { - label: "Name", - props: { - index: 0, - rowSpan: 1, - colSpan: 1, - sort: true - }, - transforms: [sortableTransform], - formatters: [sortingFormatter], - customFormatters: [sortableHeaderCellFormatter] - }, - cell: { - props: { - index: 0 - }, - formatters: [tableCellFormatter] - } - }, - { - property: "title", - header: { - label: "Position Title", - props: { - index: 1, - rowSpan: 1, - colSpan: 1, - sort: true - }, - transforms: [sortableTransform], - formatters: [sortingFormatter], - customFormatters: [sortableHeaderCellFormatter] - }, - cell: { - props: { - index: 1 - }, - formatters: [tableCellFormatter] - } - }, - { - property: "date_joined", - header: { - label: "Date Joined", - props: { - index: 2, - rowSpan: 1, - colSpan: 1, - sort: true - }, - transforms: [sortableTransform], - formatters: [sortingFormatter], - customFormatters: [sortableHeaderCellFormatter] - }, - cell: { - props: { - index: 2 - }, - formatters: [tableCellFormatter] - } - }, - { - property: "actions", - header: { - label: "Actions", - props: { - index: 3, - rowSpan: 1, - colSpan: 2 - }, - formatters: [actionHeaderCellFormatter] - }, - cell: { - props: { - index: 3 - }, - formatters: [ - (value, { rowData }) => [ - - console.log(`clicked ${rowData.name}`)} - > - Edit - - , - - - Action - Another Action - Something else here - - Separated link - - - ] - ] - } - } - ], - rows: [{}] - }; - } - - render() { - const { plugins } = this.props; - const { rows, sortingColumns, columns } = this.state; - const showEmptyState = isEmpty(plugins); - - const sortedRows = sort.sorter({ - columns, - sortingColumns, - sort: orderBy, - strategy: sort.strategies.byProperty - })(rows); - - return ( - - - - Teammates - - {showEmptyState ? ( - - ) : ( - - - this.customHeaderFormatters({ - cellProps, - columns, - sortingColumns - }) - } - }} - > - - ({ - role: "row" - })} - /> - - - )} - - {!showEmptyState && ( - - - - Add Teammate - - - - )} - - - - ); - } -} -DashTeamView.propTypes = { - plugins: PropTypes.arrayOf(PropTypes.object) -}; - -DashTeamView.defaultProps = { - plugins: [] -}; - -export default DashTeamView; diff --git a/src/components/Dashboard/components/DashTeamView/DashTeamView.jsx b/src/components/Dashboard/components/DashTeamView/DashTeamView.jsx new file mode 100644 index 00000000..c7a325b5 --- /dev/null +++ b/src/components/Dashboard/components/DashTeamView/DashTeamView.jsx @@ -0,0 +1,170 @@ +import React, { Component } from 'react'; +import isEmpty from 'lodash/isEmpty'; +import PropTypes from 'prop-types'; +import { + Table, + TableHeader, + TableBody, + sortable, + SortByDirection, + headerCol, + info, +} from '@patternfly/react-table'; +import { + CardTitle, + CardBody, + Card, + CardFooter, + Grid, + GridItem, + CardActions, + Button, +} from '@patternfly/react-core'; + +import { PlusCircleIcon } from '@patternfly/react-icons'; +import BrainyTeammatesPointer from '../../../../assets/img/brainy_teammates-pointer.png'; +import './DashTeamView.css'; + +const DashTeamEmptyState = () => ( +
+

+ In this area, you will be able to add and manage teammates to help you + with each plugin. +

+ Click Add Plugin +
+); + +class DashTeamView extends Component { + constructor(props) { + super(props); + this.state = { + rows: [], + columns: [ + { + title: 'Name', + property: 'name', + transforms: [sortable, headerCol()], + }, + { + title: 'Position Title', + property: 'title', + transforms: [sortable], + }, + { + title: 'Date Joined', + property: 'date_joined', + transforms: [ + info({ + tooltip: 'More information about teammates', + }), + sortable, + ], + }, + ], + sortBy: {}, + }; + + this.state.rows = props.plugins.map((plugin) => { + const row = []; + const { columns } = this.state; + row.push(...columns.map(({ property }) => plugin[property])); + return row; + }); + + this.onSort = this.onSort.bind(this); + } + + onSort(_event, index, direction) { + this.setState((prevState) => { + // eslint-disable-next-line no-nested-ternary + const sortedRows = prevState.rows.sort((a, b) => (a[index] < b[index] ? -1 : a[index] > b[index] ? 1 : 0)); + return { + sortBy: { + index, + direction, + }, + rows: direction === SortByDirection.asc ? sortedRows : sortedRows.reverse(), + } + }); + } + + render() { + const { plugins } = this.props; + const { rows, columns, sortBy } = this.state; + const showEmptyState = isEmpty(plugins); + + return ( + + + + Teammates + + {showEmptyState ? ( + + ) : ( + <> + Link action, + }, + { + title: 'Some action', + // eslint-disable-next-line no-unused-vars + onClick: (event, rowId, rowData, extra) => { + // console.log('clicked on Some action, on row: ', rowId); + }, + }, + { + title: 'Third action', + // eslint-disable-next-line no-unused-vars + onClick: (event, rowId, rowData, extra) => { + // console.log('clicked on Third action, on row: ', rowId); + }, + }, + ])} + areActionsDisabled={false} + dropdownPosition="left" + dropdownDirection="bottom" + > + + +
+ + )} +
+ {!showEmptyState && ( + + + + + + + )} +
+
+
+ ); + } +} + +DashTeamView.propTypes = { + plugins: PropTypes.arrayOf(PropTypes.object), +}; + +DashTeamView.defaultProps = { + plugins: [], +}; + +export default DashTeamView; diff --git a/src/components/Developers/Developers.js b/src/components/Developers/Developers.jsx similarity index 100% rename from src/components/Developers/Developers.js rename to src/components/Developers/Developers.jsx diff --git a/src/components/Developers/Developers.test.js b/src/components/Developers/Developers.test.jsx similarity index 100% rename from src/components/Developers/Developers.test.js rename to src/components/Developers/Developers.test.jsx diff --git a/src/components/Developers/__snapshots__/Developers.test.jsx.snap b/src/components/Developers/__snapshots__/Developers.test.jsx.snap new file mode 100644 index 00000000..6395ec44 --- /dev/null +++ b/src/components/Developers/__snapshots__/Developers.test.jsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Developers should render correctly 1`] = `ShallowWrapper {}`; diff --git a/src/components/Developers/components/BashLine/BashLine.js b/src/components/Developers/components/BashLine/BashLine.js deleted file mode 100644 index 2d0c5391..00000000 --- a/src/components/Developers/components/BashLine/BashLine.js +++ /dev/null @@ -1,26 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import CopyToClipboard from '../CopyToClipboard/CopyToClipboard'; -import './BashLine.css'; - -class BashLine extends Component { - constructor() { - super(); - this.tooltipShown = false; - } - - render() { - return ( -
-
- {`$ ${this.props.command}`} -
- -
- ); - } -} - -BashLine.propTypes = { command: PropTypes.string.isRequired }; - -export default BashLine; diff --git a/src/components/Developers/components/BashLine/BashLine.jsx b/src/components/Developers/components/BashLine/BashLine.jsx new file mode 100644 index 00000000..650b6a93 --- /dev/null +++ b/src/components/Developers/components/BashLine/BashLine.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Button, clipboardCopyFunc, Tooltip } from '@patternfly/react-core'; +import { CopyIcon } from '@patternfly/react-icons'; +import PropTypes from 'prop-types'; +import './BashLine.css'; + +function BashLine({ command }) { + return ( +
+
+ {`$ ${command}`} +
+ + + +
+ ); +} + +BashLine.propTypes = { command: PropTypes.string.isRequired }; + +export default BashLine; diff --git a/src/components/Developers/components/BashLine/BashLine.test.js b/src/components/Developers/components/BashLine/BashLine.test.jsx similarity index 100% rename from src/components/Developers/components/BashLine/BashLine.test.js rename to src/components/Developers/components/BashLine/BashLine.test.jsx diff --git a/src/components/Developers/components/BashLine/__snapshots__/BashLine.test.js.snap b/src/components/Developers/components/BashLine/__snapshots__/BashLine.test.jsx.snap similarity index 100% rename from src/components/Developers/components/BashLine/__snapshots__/BashLine.test.js.snap rename to src/components/Developers/components/BashLine/__snapshots__/BashLine.test.jsx.snap diff --git a/src/components/Developers/components/CopyToClipboard/CopyToClipboard.css b/src/components/Developers/components/CopyToClipboard/CopyToClipboard.css deleted file mode 100644 index 83a120f4..00000000 --- a/src/components/Developers/components/CopyToClipboard/CopyToClipboard.css +++ /dev/null @@ -1,13 +0,0 @@ -.copy-to-clipboard-btn { - /* color */ - background: #fafafa; - background: var(--pf-black-100); - - /* display */ - padding: 5px 9px !important; - - /* position */ - position: absolute; - top: 7px; - right: 7px; -} diff --git a/src/components/Developers/components/CopyToClipboard/CopyToClipboard.js b/src/components/Developers/components/CopyToClipboard/CopyToClipboard.js deleted file mode 100644 index d6a30404..00000000 --- a/src/components/Developers/components/CopyToClipboard/CopyToClipboard.js +++ /dev/null @@ -1,46 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import Clipboard from 'react-clipboard.js'; -import { Icon, OverlayTrigger, Tooltip } from 'patternfly-react'; -import './CopyToClipboard.css'; - -class CopyToClipboard extends Component { - constructor(props) { - super(props); - this.overlayTrigger = React.createRef(); - this.showTooltip = this.showTooltip.bind(this); - } - - showTooltip() { - this.overlayTrigger.current.show(); - setTimeout(() => { this.overlayTrigger.current.hide(); }, 600); - } - - render() { - return ( - - Copied! - - )} - > - - - - - ); - } -} - -CopyToClipboard.propTypes = { clipboardText: PropTypes.string.isRequired }; - -export default CopyToClipboard; diff --git a/src/components/Developers/components/CopyToClipboard/CopyToClipboard.test.js b/src/components/Developers/components/CopyToClipboard/CopyToClipboard.test.js deleted file mode 100644 index d3447916..00000000 --- a/src/components/Developers/components/CopyToClipboard/CopyToClipboard.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { mount, shallow } from 'enzyme'; -import Clipboard from 'react-clipboard.js'; -import CopyToClipboard from './CopyToClipboard'; - - -describe('CopyToClipboard', () => { - let wrapper; - beforeEach(() => { - wrapper = shallow(); - }); - - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should render Clipboard component', () => { - expect(wrapper.find(Clipboard)).toHaveLength(1); - }); - - it('should render copy Icon component with name', () => { - expect(wrapper.find('Icon').prop('name')).toEqual('copy'); - }); - - it('should render OverlayTrigger component', () => { - expect(wrapper.find('OverlayTrigger')).toHaveLength(1); - }); -}); - -describe('mounted CopyToClipboard', () => { - let wrapper; - beforeEach(() => { - wrapper = mount(); - }); - - it('renders the data-clipboard-text attribute', () => { - wrapper.setProps({ clipboardText: 'text' }); - const clipboardElement = wrapper.find(Clipboard); - expect(clipboardElement.prop('data-clipboard-text')).toEqual('text'); - }); - - it('should show tooltip when the copy button is clicked', () => { - const overlayTrigger = wrapper.find('OverlayTrigger'); - overlayTrigger.simulate('click'); - expect(overlayTrigger.instance().state.show).toEqual(true); - }); -}); diff --git a/src/components/Developers/components/CopyToClipboard/__snapshots__/CopyToClipboard.test.js.snap b/src/components/Developers/components/CopyToClipboard/__snapshots__/CopyToClipboard.test.js.snap deleted file mode 100644 index cf1dee3b..00000000 --- a/src/components/Developers/components/CopyToClipboard/__snapshots__/CopyToClipboard.test.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CopyToClipboard should render correctly 1`] = ` -ShallowWrapper { - "length": 1, -} -`; diff --git a/src/components/Developers/components/DeveloperCTA/DeveloperCTA.css b/src/components/Developers/components/DeveloperCTA/DeveloperCTA.css index 12839e28..1a312b47 100644 --- a/src/components/Developers/components/DeveloperCTA/DeveloperCTA.css +++ b/src/components/Developers/components/DeveloperCTA/DeveloperCTA.css @@ -1,5 +1,7 @@ -.developer-cta { +#developer-cta { /* color */ + padding: 6em 2em; + background: #393f44; background: var(--pf-black-800); background: linear-gradient(135deg, #4d5258, #393f44); @@ -8,41 +10,16 @@ background-repeat: no-repeat; background-size: cover; background-position: center; - - /* display */ - padding: 20px; + padding: 6em 3em; } -.developer-cta-overview, .developer-cta-form { margin: 15px; } - -.developer-cta-overview { flex: 3; } - -.developer-cta-form { flex: 2; } - -.developer-cta-header, .developer-cta-desc { - /* color */ +#developer-cta-header { color: #fafafa; - color: var(--pf-black-100); } -.developer-cta-header { - font-weight: 200; - font-size: 3.4em; - line-height: 1.3; -} - -.developer-cta-desc { - /* display */ - margin-top: 20px; - - /* font */ +#developer-cta-header h2 { + font-size: 3em; + line-height: 1.5; font-weight: 600; - font-size: medium; - text-shadow: 3px 3px 6px #030303; - text-shadow: 3px 3px 6px var(--pf-black); -} - -/* responsive breakpoints */ -@media (min-width: 480px) { - .developer-cta { padding: 4em; } + margin-bottom: 1em; } diff --git a/src/components/Developers/components/DeveloperCTA/DeveloperCTA.js b/src/components/Developers/components/DeveloperCTA/DeveloperCTA.js deleted file mode 100644 index 6b2c8b4e..00000000 --- a/src/components/Developers/components/DeveloperCTA/DeveloperCTA.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { Card, CardBody } from '@patternfly/react-core'; -import PropTypes from 'prop-types'; -import './DeveloperCTA.css'; -import ConnectedDeveloperSignup from '../DeveloperSignup/DeveloperSignup'; -import ChrisStore from '../../../../store/ChrisStore'; - -export const DeveloperCTA = ({ store }) => ( -
-
-
-
- Expand the reach of your image processing software -
-
-

- ChRIS is an open source platform for medical - analytics in the cloud, democratizing the development of image - processing apps within an ecosystem following - - {' '}common standards, rather than disparate silos - . -

- -

- A ChRIS Developer account enables you to submit your image - processing application as a containerized ChRIS plugin and - share it with the broader ChRIS community of researchers and - clinicians. Join us! -

-
-
- { - store.get('isLoggedIn') ? - null - : -
- - - - - -
- } -
-
-); - -export default ChrisStore.withStore(DeveloperCTA); - -DeveloperCTA.propTypes = { - store: PropTypes.objectOf(PropTypes.object).isRequired, -}; diff --git a/src/components/Developers/components/DeveloperCTA/DeveloperCTA.jsx b/src/components/Developers/components/DeveloperCTA/DeveloperCTA.jsx new file mode 100644 index 00000000..5026e400 --- /dev/null +++ b/src/components/Developers/components/DeveloperCTA/DeveloperCTA.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './DeveloperCTA.css'; + +import { Card, CardBody, Grid, GridItem, Bullseye } from '@patternfly/react-core'; +import ConnectedDeveloperSignup from '../DeveloperSignup/DeveloperSignup'; +import ChrisStore from '../../../../store/ChrisStore'; + +export const DeveloperCTA = ({ store }) => ( + + + +
+

Expand the reach of your image processing software

+

+ A ChRIS Developer account enables you to submit your image + processing application as a containerized ChRIS plugin and + share it with the broader ChRIS community of researchers and + clinicians. +

+
+
+ + + { + store.get('isLoggedIn') ? null : +
+ + +

Create a ChRIS Developer Account

+ +
+
+
+ } +
+
+
+); + +export default ChrisStore.withStore(DeveloperCTA); + +DeveloperCTA.propTypes = { + store: PropTypes.objectOf(PropTypes.object).isRequired, +}; diff --git a/src/components/Developers/components/DeveloperCTA/DeveloperCTA.test.js b/src/components/Developers/components/DeveloperCTA/DeveloperCTA.test.jsx similarity index 100% rename from src/components/Developers/components/DeveloperCTA/DeveloperCTA.test.js rename to src/components/Developers/components/DeveloperCTA/DeveloperCTA.test.jsx diff --git a/src/components/Developers/components/DeveloperCTA/__snapshots__/DeveloperCTA.test.js.snap b/src/components/Developers/components/DeveloperCTA/__snapshots__/DeveloperCTA.test.jsx.snap similarity index 100% rename from src/components/Developers/components/DeveloperCTA/__snapshots__/DeveloperCTA.test.js.snap rename to src/components/Developers/components/DeveloperCTA/__snapshots__/DeveloperCTA.test.jsx.snap diff --git a/src/components/Developers/components/DeveloperSignup/DeveloperSignup.css b/src/components/Developers/components/DeveloperSignup/DeveloperSignup.css index e020a414..061e392a 100644 --- a/src/components/Developers/components/DeveloperSignup/DeveloperSignup.css +++ b/src/components/Developers/components/DeveloperSignup/DeveloperSignup.css @@ -1,3 +1,11 @@ -.developer-signup-creating { +#developer-signup-form { + color: black; +} + +#developer-signup-creating { vertical-align: super; } + +#developer-signup-creating { + vertical-align: super; +} \ No newline at end of file diff --git a/src/components/Developers/components/DeveloperSignup/DeveloperSignup.js b/src/components/Developers/components/DeveloperSignup/DeveloperSignup.jsx similarity index 61% rename from src/components/Developers/components/DeveloperSignup/DeveloperSignup.js rename to src/components/Developers/components/DeveloperSignup/DeveloperSignup.jsx index efff83c4..2d1aeabf 100644 --- a/src/components/Developers/components/DeveloperSignup/DeveloperSignup.js +++ b/src/components/Developers/components/DeveloperSignup/DeveloperSignup.jsx @@ -1,32 +1,19 @@ import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; import PropTypes from 'prop-types'; +import _ from 'lodash'; +import { validate } from 'email-validator'; import { Form, Spinner, } from '@patternfly/react-core'; -import Button from '../../../Button'; -import _ from 'lodash'; -import StoreClient from '@fnndsc/chrisstoreapi'; -import { validate } from 'email-validator'; + import './DeveloperSignup.css'; +import StoreClient from '@fnndsc/chrisstoreapi'; +import Button from '../../../Button'; import ChrisStore from '../../../../store/ChrisStore'; import FormInput from '../../../FormInput'; - -/* inspired by https://github.com/Modernizr/Modernizr/blob/v3/feature-detects/touchevents.js */ -const isTouchDevice = () => { - if (('ontouchstart') in window || (window.DocumentTouch && document instanceof window.DocumentTouch)) { - return true; - } - - if (window.matchMedia) { - const prefixes = ' -webkit- -moz- -o- -ms- '.split(' '); - const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join(''); - return window.matchMedia(query).matches; - } - - return false; -}; +import isTouchDevice from './isTouchDevice'; export class DeveloperSignup extends Component { constructor(props) { @@ -36,13 +23,13 @@ export class DeveloperSignup extends Component { this.state = { loading: false, error: { - message: '', - controls: '', + message: String(), + controls: [], }, - username: '', - email:'', - password: '', - passwordConfirm: '', + username: String(), + email: String(), + password: String(), + passwordConfirm: String(), }; } @@ -51,11 +38,9 @@ export class DeveloperSignup extends Component { } handleSubmit(event) { - const { - username, email, password, passwordConfirm, - } = this.state; + event.persist(); + const { username, email, password, passwordConfirm } = this.state; const { store } = this.props; - event.preventDefault(); if (!username) { return this.setState({ @@ -113,7 +98,7 @@ export class DeveloperSignup extends Component { return this.handleStoreLogin(); } - handleStoreLogin() { + async handleStoreLogin() { const { username, email, password } = this.state; const { store } = this.props; const storeURL = process.env.REACT_APP_STORE_URL; @@ -121,46 +106,46 @@ export class DeveloperSignup extends Component { const authURL = `${storeURL}auth-token/`; let authToken; - return new Promise(async (resolve, reject) => { - try { - await StoreClient.createUser(usersURL, username, password, email); - } catch (e) { - if (_.has(e, 'response')) { - if (_.has(e, 'response.data.username')) { - this.setState({ - loading: false, - error: { - message: 'This username is already registered.', - controls: ['username'], - }, - }); - } else { - this.setState({ - loading: false, - error: { - message: 'This email is already registered.', - controls: ['email'], - }, - }); - } + try { + await StoreClient.createUser(usersURL, username, password, email); + } catch (e) { + if (_.has(e, 'response')) { + if (_.has(e, 'response.data.username')) { + this.setState({ + loading: false, + error: { + message: 'This username is already registered.', + controls: ['username'], + }, + }); } else { this.setState({ loading: false, + error: { + message: 'This email is already registered.', + controls: ['email'], + }, }); } - return resolve(e); + } else { + this.setState({ + loading: false, + }); } - try { - authToken = await StoreClient.getAuthToken(authURL, username, password); - } catch (e) { - return reject(e); - } - return this.setState({ - toDashboard: true, - }, () => { - store.set('authToken')(authToken); - return resolve(authToken); - }); + return e; + } + + try { + authToken = await StoreClient.getAuthToken(authURL, username, password); + } catch (e) { + return Promise.reject(e); + } + + return this.setState({ + toDashboard: true, + }, () => { + store.set('authToken')(authToken); + return authToken; }); } @@ -169,24 +154,28 @@ export class DeveloperSignup extends Component { error, loading, toDashboard, + + username, + email, + password, + passwordConfirm, } = this.state; - if (toDashboard) { + if (toDashboard) return ; - } + const disableControls = loading; return ( -
-

{loading ? 'Creating' : 'Create'} a ChRIS Developer account:

+ this.handleChange(val, 'username')} disableControls={disableControls} @@ -196,11 +185,11 @@ export class DeveloperSignup extends Component { formLabel="Email" fieldId="email" validationState={error.controls.includes('email') ? 'error' : 'default'} - helperText="Enter you email" + placeholder="Enter your email" inputType="email" id="email" fieldName="email" - value={this.state.email} + value={email} onChange={(val) => this.handleChange(val, 'email')} disableControls={disableControls} error={error} @@ -209,39 +198,42 @@ export class DeveloperSignup extends Component { formLabel="Password" fieldId="password" validationState={error.controls.includes('password') ? 'error' : 'default'} - helperText="Enter your password" + placeholder="Enter a password" inputType="password" id="password" fieldName="password" - value={this.state.password} + value={password} onChange={(val) => this.handleChange(val, 'password')} disableControls={disableControls} error={error} /> this.handleChange(val, 'passwordConfirm')} disableControls={disableControls} error={error} /> - {loading ? : ( - - )} - {loading && Creating Account} - ); +
+ {loading ? : ( + + )} + {loading && Creating Account} +
+ + ); } } diff --git a/src/components/Developers/components/DeveloperSignup/DeveloperSignup.test.js b/src/components/Developers/components/DeveloperSignup/DeveloperSignup.test.jsx similarity index 100% rename from src/components/Developers/components/DeveloperSignup/DeveloperSignup.test.js rename to src/components/Developers/components/DeveloperSignup/DeveloperSignup.test.jsx diff --git a/src/components/Developers/components/DeveloperSignup/__snapshots__/DeveloperSignup.test.js.snap b/src/components/Developers/components/DeveloperSignup/__snapshots__/DeveloperSignup.test.jsx.snap similarity index 100% rename from src/components/Developers/components/DeveloperSignup/__snapshots__/DeveloperSignup.test.js.snap rename to src/components/Developers/components/DeveloperSignup/__snapshots__/DeveloperSignup.test.jsx.snap diff --git a/src/components/Developers/components/DeveloperSignup/isTouchDevice.js b/src/components/Developers/components/DeveloperSignup/isTouchDevice.js new file mode 100644 index 00000000..54e2070f --- /dev/null +++ b/src/components/Developers/components/DeveloperSignup/isTouchDevice.js @@ -0,0 +1,16 @@ +/* inspired by https://github.com/Modernizr/Modernizr/blob/v3/feature-detects/touchevents.js */ +const isTouchDevice = () => { + if (('ontouchstart') in window || (window.DocumentTouch && document instanceof window.DocumentTouch)) { + return true; + } + + if (window.matchMedia) { + const prefixes = ' -webkit- -moz- -o- -ms- '.split(' '); + const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join(''); + return window.matchMedia(query).matches; + } + + return false; +}; + +export default isTouchDevice \ No newline at end of file diff --git a/src/components/Developers/components/Instructions/Instructions.js b/src/components/Developers/components/Instructions/Instructions.jsx similarity index 73% rename from src/components/Developers/components/Instructions/Instructions.js rename to src/components/Developers/components/Instructions/Instructions.jsx index 50667a88..1e7de292 100644 --- a/src/components/Developers/components/Instructions/Instructions.js +++ b/src/components/Developers/components/Instructions/Instructions.jsx @@ -6,7 +6,8 @@ const Instructions = () => (

- Get Started - 4 Simple Steps{' '} + Get Started - 4 Simple Steps + {' '} [source]

@@ -17,11 +18,21 @@ const Instructions = () => (

- Create a Python3 virtual environment for + Create a + {' '} + Python3 + {' '} + virtual environment for your plugin apps and activate it.

- Install the latest Cookiecutter in
chrisapp_env
if you haven{"'"}t installed it yet: + Install the latest Cookiecutter in + {' '} +
chrisapp_env
+ {' '} + if you haven + ' + t installed it yet:

@@ -60,9 +71,12 @@ const Instructions = () => (

- Create a new repository on{' '} + Create a new repository on + {' '} https://github.com - /your_github_username with the same name as the + /your_github_username + {' '} + with the same name as the app project directory generated by the previous cookiecutter command.

@@ -96,7 +110,11 @@ const Instructions = () => ( />

Add the URL for the remote Github repository created - in
step 2
where your local repository will be pushed: + in + {' '} +
step 2
+ {' '} + where your local repository will be pushed:

@@ -113,8 +131,11 @@ const Instructions = () => (

- Create a new automated build and repository on your{' '} - Docker Hub account. + Create a new automated build and repository on your + {' '} + Docker Hub + {' '} + account.

Once you log in, click the Create button in the header and select @@ -128,20 +149,43 @@ const Instructions = () => (


- For more information on Automated Builds, visit the{' '} - Docker build documentation. + For more information on Automated Builds, visit the + {' '} + Docker build documentation + .

- Modify
requirements.txt
,
Dockerfile
and the + Modify + {' '} +
requirements.txt
+ , + {' '} +
Dockerfile
+ {' '} + and the Python code with the proper versions of Python dependencies and libraries and push your changes to Github.

- Look at this{' '} - simple fs plugin - {' '}or this{' '} - simple ds plugin - {' '}for guidance on getting started with your ChRIS plugin. + Look at this + {' '} + + simple + fs + {' '} + plugin + + {' '} + or this + {' '} + + simple + ds + {' '} + plugin + + {' '} + for guidance on getting started with your ChRIS plugin.

@@ -152,8 +196,14 @@ const Instructions = () => (

- Once you{"'"}ve developed and properly tested your plugin app - consult the wiki to learn how to register it to + Once you + ' + ve developed and properly tested your plugin app + consult the + {' '} + wiki + {' '} + to learn how to register it to ChRIS.

diff --git a/src/components/Developers/components/Instructions/Instructions.test.js b/src/components/Developers/components/Instructions/Instructions.test.jsx similarity index 83% rename from src/components/Developers/components/Instructions/Instructions.test.js rename to src/components/Developers/components/Instructions/Instructions.test.jsx index de023410..a25d083f 100644 --- a/src/components/Developers/components/Instructions/Instructions.test.js +++ b/src/components/Developers/components/Instructions/Instructions.test.jsx @@ -2,7 +2,6 @@ import React from 'react'; import { shallow } from 'enzyme'; import Instructions from './Instructions'; - describe('Instructions', () => { let wrapper; beforeEach(() => { @@ -28,21 +27,21 @@ describe('Instructions', () => { it('should have a number to go with every instruction step', () => { expect(Array .from(wrapper.find('div.instructions-step')) - .every(step => shallow(step).find('div.instructions-number').length > 0)) + .every((step) => shallow(step).find('div.instructions-number').length > 0)) .toEqual(true); }); it('should have a sub to go with every instruction number', () => { expect(Array .from(wrapper.find('div.instructions-step')) - .every(step => shallow(step).find('div.instructions-number > sub').length > 0)) + .every((step) => shallow(step).find('div.instructions-number > sub').length > 0)) .toEqual(true); }); it('should have a body to go with every instruction step', () => { expect(Array .from(wrapper.find('div.instructions-step')) - .every(step => shallow(step).find('div.instructions-body').length > 0)) + .every((step) => shallow(step).find('div.instructions-body').length > 0)) .toEqual(true); }); }); diff --git a/src/components/Footer/Footer.css b/src/components/Footer/Footer.css index 8b707c89..7f414e03 100644 --- a/src/components/Footer/Footer.css +++ b/src/components/Footer/Footer.css @@ -2,13 +2,13 @@ /* color */ background: #292e34; background: var(--pf-black-900); - background: linear-gradient(#292e34, #030303); - background: linear-gradient(var(--pf-black-900), var(--pf-black)); + /* background: linear-gradient(#292e34, #030303); + background: linear-gradient( var(--pf-black) 25%, var(--pf-black-900)); */ color: #fafafa; color: var(--pf-black-100); /* display */ - padding: 4em 1em; + padding: 5em 1em; padding-bottom: 8em; /* position */ @@ -21,7 +21,7 @@ .footer-row { /* display */ width: 100%; - max-width: 1500px; + max-width: 1200px; margin: 0 auto !important; } @@ -92,12 +92,13 @@ .footer-copyright { /* position */ position: absolute; - bottom: 1em; + bottom: 2em; left: 0; right: 0; /* font */ text-align: center; + font-size: 0.8em; } /* ============================== */ diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.jsx similarity index 83% rename from src/components/Footer/Footer.js rename to src/components/Footer/Footer.jsx index 873c1704..c98ca156 100644 --- a/src/components/Footer/Footer.js +++ b/src/components/Footer/Footer.jsx @@ -1,6 +1,6 @@ -import React from "react"; -import "./Footer.css"; -import chrisLogo from "../../assets/img/chris_logo-white.png"; +import React from 'react'; +import './Footer.css'; +import chrisLogo from '../../assets/img/chris_logo-white.png'; const Footer = () => (
@@ -10,7 +10,9 @@ const Footer = () => (
- ChRIS is developed by Boston Children{"'"}s Hospital in partnership + ChRIS is developed by Boston Children + ' + s Hospital in partnership with Red Hat, the Massachusetts Open Cloud (MOC), and Boston University.
@@ -31,7 +33,7 @@ const Footer = () => ( Report an Issue
-
+
Contribute @@ -48,7 +50,7 @@ const Footer = () => (
- © 2018 - {new Date().getFullYear()} Boston Children{"'"}s Hospital, Red + © 2018 - {new Date().getFullYear()} Boston Children's Hospital, Red Hat, Massachusetts Open Cloud, Boston University. All rights reserved.
diff --git a/src/components/Footer/Footer.test.js b/src/components/Footer/Footer.test.jsx similarity index 100% rename from src/components/Footer/Footer.test.js rename to src/components/Footer/Footer.test.jsx diff --git a/src/components/Footer/__snapshots__/Footer.test.js.snap b/src/components/Footer/__snapshots__/Footer.test.jsx.snap similarity index 100% rename from src/components/Footer/__snapshots__/Footer.test.js.snap rename to src/components/Footer/__snapshots__/Footer.test.jsx.snap diff --git a/src/components/FormInput/index.js b/src/components/FormInput/index.jsx similarity index 79% rename from src/components/FormInput/index.js rename to src/components/FormInput/index.jsx index 1fc4aed6..0b1a4a22 100644 --- a/src/components/FormInput/index.js +++ b/src/components/FormInput/index.jsx @@ -27,15 +27,17 @@ const FormInput = (props) => { validated={validationState} className={className} helperText={ - helperText ? - - {helperText} - : null + helperText + ? ( + + {helperText} + + ) : null } - helperTextInvalid={error && error.controls.includes(fieldName) ? error.message: null} + helperTextInvalid={error && error.controls.includes(fieldName) ? error.message : null} > - { - children ? children : ( + { + children || ( { /> ) } - + - ) + ); }; -export default FormInput; \ No newline at end of file +export default FormInput; diff --git a/src/components/Hintblock/hint.css b/src/components/Hintblock/hint.css deleted file mode 100644 index fd907149..00000000 --- a/src/components/Hintblock/hint.css +++ /dev/null @@ -1,6 +0,0 @@ -.hint-block .pf-c-hint__body{ - font-size: 13px; - font-weight: 300; - line-height: 1.4; - color: #004368; -} \ No newline at end of file diff --git a/src/components/Hintblock/index.js b/src/components/Hintblock/index.js deleted file mode 100644 index 2f76447c..00000000 --- a/src/components/Hintblock/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import { Hint, HintBody, HintFooter, HintTitle } from '@patternfly/react-core'; -import './hint.css'; -const HintBlock = ({hintActions, hintBody, hintFooter, hintTitle, ...props})=>{ - return ( - - {hintTitle && ({hintTitle})} - {hintBody} - {hintFooter && ({hintFooter})} - - ) -} -export default HintBlock; \ No newline at end of file diff --git a/src/components/LoadingContainer/LoadingContainer.js b/src/components/LoadingContainer/LoadingContainer.jsx similarity index 75% rename from src/components/LoadingContainer/LoadingContainer.js rename to src/components/LoadingContainer/LoadingContainer.jsx index 3d371e2e..005c1777 100644 --- a/src/components/LoadingContainer/LoadingContainer.js +++ b/src/components/LoadingContainer/LoadingContainer.jsx @@ -2,9 +2,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import './LoadingContainer.css'; -const LoadingContainer = props => ( -
- {props.children} +const LoadingContainer = ({ className, children }) => ( +
+ {children}
); diff --git a/src/components/LoadingContainer/LoadingContainer.test.js b/src/components/LoadingContainer/LoadingContainer.test.jsx similarity index 100% rename from src/components/LoadingContainer/LoadingContainer.test.js rename to src/components/LoadingContainer/LoadingContainer.test.jsx diff --git a/src/components/LoadingContainer/__snapshots__/LoadingContainer.test.js.snap b/src/components/LoadingContainer/__snapshots__/LoadingContainer.test.jsx.snap similarity index 100% rename from src/components/LoadingContainer/__snapshots__/LoadingContainer.test.js.snap rename to src/components/LoadingContainer/__snapshots__/LoadingContainer.test.jsx.snap diff --git a/src/components/LoadingContainer/components/LoadingContent/LoadingContent.js b/src/components/LoadingContainer/components/LoadingContent/LoadingContent.jsx similarity index 72% rename from src/components/LoadingContainer/components/LoadingContent/LoadingContent.js rename to src/components/LoadingContainer/components/LoadingContent/LoadingContent.jsx index f6e652bb..e6b1b9e7 100644 --- a/src/components/LoadingContainer/components/LoadingContent/LoadingContent.js +++ b/src/components/LoadingContainer/components/LoadingContent/LoadingContent.jsx @@ -1,18 +1,27 @@ import React from 'react'; import PropTypes from 'prop-types'; -const LoadingContent = (props) => { +const LoadingContent = ({ + width, + height, + top, + left, + bottom, + right, + className, + type, +}) => { const computedStyle = { - width: props.width, - height: props.height, - marginTop: props.top, - marginLeft: props.left, - marginBottom: props.bottom, - marginRight: props.right, + width, + height, + marginTop: top, + marginLeft: left, + marginBottom: bottom, + marginRight: right, }; - let addedClasses = props.className; - switch (props.type) { + let addedClasses = className; + switch (type) { case 'white': addedClasses += ' white'; break; diff --git a/src/components/LoadingContainer/components/LoadingContent/LoadingContent.test.js b/src/components/LoadingContainer/components/LoadingContent/LoadingContent.test.jsx similarity index 100% rename from src/components/LoadingContainer/components/LoadingContent/LoadingContent.test.js rename to src/components/LoadingContainer/components/LoadingContent/LoadingContent.test.jsx diff --git a/src/components/LoadingContainer/components/LoadingContent/__snapshots__/LoadingContent.test.js.snap b/src/components/LoadingContainer/components/LoadingContent/__snapshots__/LoadingContent.test.jsx.snap similarity index 100% rename from src/components/LoadingContainer/components/LoadingContent/__snapshots__/LoadingContent.test.js.snap rename to src/components/LoadingContainer/components/LoadingContent/__snapshots__/LoadingContent.test.jsx.snap diff --git a/src/components/Navbar/Navbar.css b/src/components/Navbar/Navbar.css index e174bac3..33c690c4 100644 --- a/src/components/Navbar/Navbar.css +++ b/src/components/Navbar/Navbar.css @@ -216,9 +216,10 @@ header > .navbar-dropdown { /* font */ font-size: 1.5em; } -.login-button .other-button{ - background: #06c !important; - color: white !important; + +#login-button { + color: var(--pf-c-button--m-primary--BackgroundColor); + background-color: var(--pf-c-button--m-primary--Color); } /* ===== ACTIVE DROPDOWN BTN ==== */ @@ -321,9 +322,9 @@ header > .navbar-dropdown { } } @media (max-width: 480px){ - .login-button .other-button{ - width:70px; - height:auto; - font-size:0.9em; - } + .login-button { + width:70px; + height:auto; + font-size:0.9em; } +} diff --git a/src/components/Navbar/Navbar.js b/src/components/Navbar/Navbar.js deleted file mode 100644 index 46ee254e..00000000 --- a/src/components/Navbar/Navbar.js +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import { - Brand, - PageHeader, - Nav, - NavList, - NavItem, - PageHeaderTools, - PageHeaderToolsItem, -} from '@patternfly/react-core'; -import Button from '../Button'; -import { NavLink } from 'react-router-dom'; -import './Navbar.css'; -import LogoImg from '../../assets/img/chris-plugin-store_logo.png'; -import ChrisStore from '../../store/ChrisStore'; - -const navLinks = [ - { - label: 'Plugins', - to: '/plugins' - }, - { - label: 'Quick Start', - to: '/quickstart' - }, - { - label: 'Dashboard', - to: '/dashboard', - cond: (store) => store.get('isLoggedIn') - } -]; - -/** - * Conditionally renders a list of links into a