From dbed06f3005f5875dc01122755fe108f203385c4 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Tue, 30 Mar 2021 15:14:02 +0200 Subject: [PATCH] Feat/material UI (#250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ivar Conradi Østhus Co-authored-by: Christopher Kolstad Co-authored-by: Christopher Kolstad --- frontend/package.json | 19 +- frontend/src/__mocks__/react-mdl.js | 37 - frontend/src/__mocks__/react-modal.js | 12 - frontend/src/app.css | 101 +- frontend/src/common.styles.js | 35 + .../ReportCard/ReportCard.jsx} | 12 +- .../ReportCard/ReportCard.module.scss | 85 ++ .../ReportCard/ReportCardContainer.jsx} | 4 +- .../ReportToggleList/ReportToggleList.jsx} | 47 +- .../ReportToggleList.module.scss | 71 ++ .../ReportToggleListContainer.jsx} | 4 +- .../ReportToggleListHeader.jsx} | 15 +- .../ReportToggleListItem.jsx} | 17 +- .../reporting.jsx => Reporting/Reporting.jsx} | 13 +- .../Reporting.module.scss} | 2 +- .../ReportingContainer.jsx} | 2 +- .../__tests__/reporting-test.js | 14 +- .../__tests__/sorting-test.js | 0 .../{reporting => Reporting}/constants.js | 0 .../{reporting => Reporting}/testData.js | 0 .../{reporting => Reporting}/useSort.js | 0 .../{reporting => Reporting}/utils.js | 0 .../component/addons/AddonList/AddonList.jsx | 70 ++ .../AvailableAddons/AvailableAddons.jsx | 44 + .../AddonList/AvailableAddons/index.jsx | 3 + .../ConfiguredAddons/ConfiguredAddons.jsx | 77 ++ .../AddonList/ConfiguredAddons/index.jsx | 3 + .../src/component/addons/AddonList/index.js | 3 + .../component/addons/form-addon-component.jsx | 68 +- .../addons/form-addon-component.module.scss | 17 + .../component/addons/form-addon-container.js | 2 +- .../component/addons/form-addon-events.jsx | 13 +- .../addons/form-addon-parameters.jsx | 20 +- .../addons/{list-container.jsx => index.jsx} | 2 +- .../src/component/addons/list-component.jsx | 117 --- .../show-api-details-component-test.jsx.snap | 62 +- .../show-api-details-component-test.jsx | 2 - .../api/show-api-details-component.jsx | 18 +- frontend/src/component/app.jsx | 5 +- .../application-edit-component-test.js.snap | 968 +++++++++++------- .../application-edit-component-test.js | 192 ++-- .../application/application-edit-component.js | 167 +-- .../application/application-edit-container.js | 4 +- .../application/application-list-component.js | 28 +- .../application/application-update.jsx | 68 +- .../application/application-view.jsx | 188 ++-- .../application/stateful-textfield.js | 35 - .../archive/archive-list-container.js | 6 +- .../src/component/archive/view-container.js | 2 +- .../ConditionallyRender.jsx} | 0 .../common/ConditionallyRender/index.jsx | 3 + .../component/common/Dialogue/Dialogue.jsx | 37 + .../src/component/common/Dialogue/index.jsx | 3 + .../common/HeaderTitle/HeaderTitle.jsx | 36 + .../component/common/HeaderTitle/index.jsx | 3 + .../component/common/HeaderTitle/styles.js | 18 + .../common/PageContent/PageContent.jsx | 51 + .../component/common/PageContent/index.jsx | 3 + .../component/common/PageContent/styles.js | 23 + .../common/ProjectSelect/ProjectSelect.jsx | 65 ++ .../ProjectSelect/index.jsx} | 8 +- .../common/SearchField/SearchField.jsx | 59 ++ .../component/common/SearchField/index.jsx | 3 + .../component/common/SearchField/styles.js | 24 + .../src/component/common/TabNav/TabNav.jsx | 63 ++ .../common/TabNav/TabPanel/TabPanel.jsx | 22 + .../common/TabNav/TabPanel/index.jsx | 3 + .../src/component/common/TabNav/index.jsx | 3 + .../src/component/common/TabNav/styles.js | 7 + .../src/component/common/common.module.scss | 40 +- .../src/component/common/dropdown-menu.jsx | 57 ++ frontend/src/component/common/index.js | 152 ++- .../src/component/common/input-list-field.jsx | 10 +- .../src/component/common/search-field.jsx | 50 - frontend/src/component/common/select.jsx | 54 +- .../src/component/context/Context.module.scss | 65 ++ .../context/ContextList/ContextList.jsx | 94 ++ .../index.jsx} | 13 +- .../component/context/ContextList/styles.js | 7 + .../context/form-context-component.jsx | 133 +-- .../src/component/context/list-component.jsx | 80 -- .../src/component/error/error-component.jsx | 23 +- .../FeatureToggleList/FeatureToggleList.jsx | 172 ++++ .../FeatureToggleListActions.jsx | 88 ++ .../FeatureToggleListActions/index.jsx | 3 + .../FeatureToggleListActions/styles.js | 10 + .../FeatureToggleListItem.jsx | 98 ++ .../FeatureToggleListItemChip.jsx | 26 + .../FeatureToggleListItemChip/index.jsx} | 2 +- .../FeatureToggleListItemChip/styles.js | 9 + .../FeatureToggleListItem/index.jsx | 3 + .../FeatureToggleListItem/styles.js | 23 + .../feature-list-item-component-test.jsx.snap | 193 ++++ .../list-component-test.jsx.snap | 381 +++++++ .../feature-list-item-component-test.jsx | 48 +- .../__tests__/list-component-test.jsx | 71 ++ .../index.jsx} | 9 +- .../FeatureToggleList/loadingFeatures.js | 134 +++ .../feature/FeatureToggleList/styles.js | 14 + .../feature/FeatureView/FeatureView.jsx | 326 ++++++ .../FeatureView/FeatureView.module.scss | 32 + .../index.jsx} | 6 +- .../__snapshots__/progress-test.jsx.snap | 14 +- .../feature/__tests__/progress-test.jsx | 2 +- .../feature/add-tag-dialog-component.jsx | 79 +- .../add-tag-dialog-component.module.scss | 3 + .../add-feature-component-test.jsx.snap | 105 -- .../__tests__/add-feature-component-test.jsx | 125 --- .../feature/create/add-feature-component.jsx | 90 +- .../create/add-feature-component.module.scss | 30 + .../feature/create/copy-feature-component.jsx | 70 +- .../create/copy-feature-component.module.scss | 28 + .../feature/feature-tag-component.jsx | 68 +- .../feature/feature-type-select-component.jsx | 21 +- .../feature-list-item-component-test.jsx.snap | 138 --- .../list-component-test.jsx.snap | 418 -------- .../list/__tests__/list-component-test.jsx | 66 -- .../feature/list/feature-type-component.jsx | 22 - .../component/feature/list/list-component.jsx | 158 --- .../feature/list/list-item-component.jsx | 86 -- .../component/feature/list/list.module.scss | 29 - .../feature/list/project-component.jsx | 68 -- .../component/feature/progress-component.jsx | 14 +- .../feature/project-select-component.jsx | 12 +- .../component/feature/status-component.jsx | 17 +- .../src/component/feature/status.module.scss | 14 + .../strategy/AddStrategy/AddStrategy.jsx | 96 ++ .../AddStrategy/AddStrategy.styles.js | 17 + .../AddStrategyCard/AddStrategyCard.jsx | 28 + .../AddStrategyCard/AddStrategyCard.styles.js | 15 + .../feature/strategy/AddStrategy/utils.js | 13 + .../EditStrategyModal/EditStrategyModal.jsx | 90 ++ .../EditStrategyModal/FlexibleStrategy.jsx | 69 ++ .../default-strategy.jsx | 0 .../general-strategy.jsx | 68 +- .../strategy/EditStrategyModal/input-list.jsx | 104 ++ .../EditStrategyModal/input-percentage.jsx | 104 ++ .../loading-strategy.jsx | 0 .../strategy-input-props.js | 1 - .../unknown-strategy.jsx | 0 .../user-with-id-strategy.jsx | 0 .../strategy/StrategyCard/StrategyCard.jsx | 51 + .../StrategyCard/StrategyCard.styles.js | 8 + .../StrategyCardContent.jsx | 42 + .../StrategyCardContentCustom.jsx | 97 ++ .../StrategyCardContentDefault.jsx | 17 + .../StrategyCardContentFlexible.jsx | 43 + .../StrategyCardContentGradRandom.jsx | 36 + .../StrategyCardContentList.jsx | 41 + .../StrategyCardContentRollout.jsx | 41 + .../StrategyCardConstraints.jsx | 61 ++ .../StrategyCardConstraints.styles.js | 34 + .../StrategyCardField/StrategyCardField.jsx | 26 + .../StrategyCardField.styles.js | 15 + .../StrategyCardList/StrategyCardList.jsx | 26 + .../StrategyCardList.styles.js | 14 + .../StrageyCardPercentage.jsx | 25 + .../StrategyCardPercentage.styles.js | 15 + .../StrategyCardHeader/StrategyCardHeader.jsx | 51 + .../StrategyCardHeader.styles.js | 28 + .../index.jsx} | 7 +- .../StrategyConstraintInput.jsx | 74 ++ .../StrategyConstraintInput/index.jsx} | 4 +- .../StrategyConstraintInputField.jsx | 118 +++ .../StrategyConstraintInputField.styles.js | 10 + .../StrategyConstraintInputField/index.jsx | 3 + .../feature/strategy/__test__/.eslintrc | 5 - .../strategy/__test__/strategy-add-test.jsx | 51 - .../__test__/strategy-input-list-test.jsx | 61 -- .../strategy-constraint-input-field.jsx | 128 --- .../constraint/strategy-constraint-input.jsx | 87 -- .../flexible-rollout-strategy-container.jsx | 10 - .../strategy/flexible-rollout-strategy.jsx | 78 -- .../component/feature/strategy/input-list.jsx | 91 -- .../feature/strategy/input-percentage.jsx | 53 - .../feature/strategy/strategies-add.jsx | 46 +- .../strategies-list-add-component.jsx | 94 -- .../strategies-list-add-container.jsx | 8 - .../strategy/strategies-list-component.jsx | 179 ++-- .../strategy/strategy-configure-component.jsx | 175 ---- .../feature/strategy/strategy.module.scss | 72 +- .../feature/tag-type-select-component.jsx | 6 +- .../feature/tag-type-select-container.jsx | 3 +- .../update-variant-component-test.jsx.snap | 506 +++++++-- .../update-variant-component-test.jsx | 1 - .../component/feature/variant/add-variant.jsx | 195 ++-- .../feature/variant/e-override-config.jsx | 70 +- .../feature/variant/override-config.jsx | 20 +- .../variant/update-variant-component.jsx | 67 +- .../variant/variant-view-component.jsx | 62 +- .../feature/variant/variant.module.scss | 20 +- .../view-component-test.jsx.snap | 685 +++++++++---- .../update-strategies-component-test.jsx | 3 - .../view/__tests__/view-component-test.jsx | 59 +- .../feature/view/metric-component.jsx | 115 ++- .../component/feature/view/metric.module.scss | 8 +- .../feature/view/status-update-component.jsx | 41 +- .../view/update-description-component.jsx | 46 +- .../update-description-component.module.scss | 3 + .../component/feature/view/view-component.jsx | 317 ------ .../component/history/history-component.jsx | 4 +- .../history/history-list-component.jsx | 79 +- .../src/component/history/history.module.scss | 15 +- frontend/src/component/layout/main.jsx | 72 +- frontend/src/component/menu/Footer/Footer.jsx | 138 +++ .../component/menu/Footer/Footer.module.scss | 14 + frontend/src/component/menu/Header/Header.jsx | 67 ++ frontend/src/component/menu/Header/index.jsx | 10 + frontend/src/component/menu/Header/styles.js | 27 + .../__snapshots__/breadcrumb-test.jsx.snap | 20 +- .../__snapshots__/drawer-test.jsx.snap | 307 +----- .../__snapshots__/footer-test.jsx.snap | 276 +---- .../menu/__tests__/breadcrumb-test.jsx | 2 +- .../component/menu/__tests__/drawer-test.jsx | 2 +- .../component/menu/__tests__/footer-test.jsx | 8 +- frontend/src/component/menu/breadcrumb.jsx | 17 +- frontend/src/component/menu/drawer.jsx | 71 +- .../src/component/menu/drawer.module.scss | 53 + frontend/src/component/menu/footer.jsx | 33 - frontend/src/component/menu/header.jsx | 56 - .../src/component/project/Project.module.scss | 31 + .../project/ProjectList/ProjectList.jsx | 100 ++ .../index.jsx} | 15 +- .../component/project/ProjectList/styles.js | 7 + .../project/form-project-component.jsx | 96 +- .../src/component/project/list-component.jsx | 114 ++- .../StrategiesList/StrategiesList.jsx | 164 +++ .../index.jsx} | 13 +- .../strategies/StrategiesList/styles.js | 12 + .../list-component-test.jsx.snap | 415 +++++--- .../strategy-details-component-test.jsx.snap | 440 +++++--- .../__tests__/list-component-test.jsx | 46 +- .../strategy-details-component-test.jsx | 31 +- .../component/strategies/form-container.js | 2 +- .../component/strategies/form-strategy.jsx | 165 +++ .../component/strategies/from-strategy.jsx | 164 --- .../component/strategies/list-component.jsx | 101 -- .../strategies/show-strategy-component.js | 68 +- .../strategies/strategies.module.scss | 50 +- .../strategies/strategy-details-component.jsx | 95 +- .../strategies/toggles-link-list.jsx | 34 + frontend/src/component/styles.module.scss | 25 +- .../component/tag-types/TagType.module.scss | 41 + .../tag-types/TagTypeList/TagTypeList.jsx | 132 +++ .../component/tag-types/TagTypeList/index.jsx | 3 + .../tag-type-create-component-test.js.snap | 372 +++---- .../tag-type-list-component-test.js.snap | 290 +++--- .../tag-type-create-component-test.js | 49 +- .../__tests__/tag-type-list-component-test.js | 50 +- .../tag-types/edit-tag-type-container.js | 4 +- .../tag-types/form-tag-type-component.js | 159 ++- .../{list-container.jsx => index.jsx} | 7 +- .../component/tag-types/list-component.jsx | 71 -- frontend/src/component/tags/Tag.module.scss | 32 + .../src/component/tags/TagList/TagList.jsx | 105 ++ .../component/tags/TagList/TagList.styles.js | 7 + frontend/src/component/tags/TagList/index.jsx | 3 + .../src/component/tags/form-tag-component.js | 42 +- .../tags/{list-container.jsx => index.jsx} | 2 +- .../src/component/tags/list-component.jsx | 71 -- .../user/PasswordAuth/PasswordAuth.jsx | 150 +++ .../user/PasswordAuth/PasswordAuth.styles.js | 18 + .../component/user/SimpleAuth/SimpleAuth.jsx | 66 ++ .../user/SimpleAuth/SimpleAuth.module.scss | 3 + .../src/component/user/SimpleAuth/index.jsx | 3 + .../user/authentication-component.jsx | 61 +- .../user/authentication-custom-component.jsx | 2 +- .../authentication-password-component.jsx | 119 --- .../user/authentication-simple-component.jsx | 50 - .../src/component/user/logout-component.jsx | 8 +- .../component/user/show-user-component.jsx | 40 +- frontend/src/component/user/user.module.scss | 21 +- frontend/src/index.jsx | 16 +- frontend/src/page/addons/index.js | 2 +- frontend/src/page/admin/admin-menu.jsx | 50 +- frontend/src/page/admin/api/api-howto.jsx | 2 +- .../src/page/admin/api/api-key-create.jsx | 63 +- frontend/src/page/admin/api/api-key-list.jsx | 118 ++- frontend/src/page/admin/api/index.js | 6 +- frontend/src/page/admin/api/secret.jsx | 4 +- frontend/src/page/admin/api/styles.js | 8 + frontend/src/page/admin/auth/google-auth.jsx | 107 +- frontend/src/page/admin/auth/index.js | 27 +- frontend/src/page/admin/auth/saml-auth.jsx | 129 +-- frontend/src/page/admin/index.js | 23 +- .../page/admin/users/UsersList/UsersList.jsx | 168 +++ .../index.js} | 6 +- .../page/admin/users/add-user-component.jsx | 85 +- .../admin/users/change-password-component.jsx | 39 +- .../page/admin/users/del-user-component.jsx | 34 + frontend/src/page/admin/users/index.js | 8 +- .../admin/users/update-user-component.jsx | 24 +- .../page/admin/users/users-list-component.jsx | 147 --- frontend/src/page/context/create.js | 2 +- frontend/src/page/context/index.js | 4 +- frontend/src/page/features/index.js | 2 +- frontend/src/page/features/show.js | 2 +- frontend/src/page/project/index.js | 2 +- frontend/src/page/reporting/index.js | 2 +- frontend/src/page/strategies/index.js | 2 +- frontend/src/page/tag-types/index.js | 2 +- frontend/src/page/tags/index.js | 2 +- frontend/src/store/api-calls/index.js | 55 + frontend/src/store/feature-toggle/actions.js | 12 +- frontend/src/store/index.js | 4 +- frontend/src/store/tag-type/api.js | 16 +- frontend/src/testIds.js | 7 + frontend/src/themes/main-theme.js | 80 ++ frontend/src/utils/strategy-names.js | 46 + frontend/typings.json | 1 - frontend/vercel.json | 10 +- frontend/webpack.config.js | 12 +- frontend/yarn.lock | 321 ++---- 313 files changed, 11408 insertions(+), 7750 deletions(-) delete mode 100644 frontend/src/__mocks__/react-mdl.js delete mode 100644 frontend/src/__mocks__/react-modal.js create mode 100644 frontend/src/common.styles.js rename frontend/src/component/{reporting/report-card.jsx => Reporting/ReportCard/ReportCard.jsx} (93%) create mode 100644 frontend/src/component/Reporting/ReportCard/ReportCard.module.scss rename frontend/src/component/{reporting/report-card-container.jsx => Reporting/ReportCard/ReportCardContainer.jsx} (83%) rename frontend/src/component/{reporting/report-toggle-list.jsx => Reporting/ReportToggleList/ReportToggleList.jsx} (68%) create mode 100644 frontend/src/component/Reporting/ReportToggleList/ReportToggleList.module.scss rename frontend/src/component/{reporting/report-toggle-list-container.jsx => Reporting/ReportToggleList/ReportToggleListContainer.jsx} (82%) rename frontend/src/component/{reporting/report-toggle-list-header.jsx => Reporting/ReportToggleList/ReportToggleListHeader/ReportToggleListHeader.jsx} (82%) rename frontend/src/component/{reporting/report-toggle-list-item.jsx => Reporting/ReportToggleList/ReportToggleListItem/ReportToggleListItem.jsx} (87%) rename frontend/src/component/{reporting/reporting.jsx => Reporting/Reporting.jsx} (79%) rename frontend/src/component/{reporting/reporting.module.scss => Reporting/Reporting.module.scss} (98%) rename frontend/src/component/{reporting/reporting-container.jsx => Reporting/ReportingContainer.jsx} (90%) rename frontend/src/component/{reporting => Reporting}/__tests__/reporting-test.js (63%) rename frontend/src/component/{reporting => Reporting}/__tests__/sorting-test.js (100%) rename frontend/src/component/{reporting => Reporting}/constants.js (100%) rename frontend/src/component/{reporting => Reporting}/testData.js (100%) rename frontend/src/component/{reporting => Reporting}/useSort.js (100%) rename frontend/src/component/{reporting => Reporting}/utils.js (100%) create mode 100644 frontend/src/component/addons/AddonList/AddonList.jsx create mode 100644 frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx create mode 100644 frontend/src/component/addons/AddonList/AvailableAddons/index.jsx create mode 100644 frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.jsx create mode 100644 frontend/src/component/addons/AddonList/ConfiguredAddons/index.jsx create mode 100644 frontend/src/component/addons/AddonList/index.js create mode 100644 frontend/src/component/addons/form-addon-component.module.scss rename frontend/src/component/addons/{list-container.jsx => index.jsx} (94%) delete mode 100644 frontend/src/component/addons/list-component.jsx delete mode 100644 frontend/src/component/application/stateful-textfield.js rename frontend/src/component/common/{conditionally-render.jsx => ConditionallyRender/ConditionallyRender.jsx} (100%) create mode 100644 frontend/src/component/common/ConditionallyRender/index.jsx create mode 100644 frontend/src/component/common/Dialogue/Dialogue.jsx create mode 100644 frontend/src/component/common/Dialogue/index.jsx create mode 100644 frontend/src/component/common/HeaderTitle/HeaderTitle.jsx create mode 100644 frontend/src/component/common/HeaderTitle/index.jsx create mode 100644 frontend/src/component/common/HeaderTitle/styles.js create mode 100644 frontend/src/component/common/PageContent/PageContent.jsx create mode 100644 frontend/src/component/common/PageContent/index.jsx create mode 100644 frontend/src/component/common/PageContent/styles.js create mode 100644 frontend/src/component/common/ProjectSelect/ProjectSelect.jsx rename frontend/src/component/{feature/list/project-container.jsx => common/ProjectSelect/index.jsx} (50%) create mode 100644 frontend/src/component/common/SearchField/SearchField.jsx create mode 100644 frontend/src/component/common/SearchField/index.jsx create mode 100644 frontend/src/component/common/SearchField/styles.js create mode 100644 frontend/src/component/common/TabNav/TabNav.jsx create mode 100644 frontend/src/component/common/TabNav/TabPanel/TabPanel.jsx create mode 100644 frontend/src/component/common/TabNav/TabPanel/index.jsx create mode 100644 frontend/src/component/common/TabNav/index.jsx create mode 100644 frontend/src/component/common/TabNav/styles.js create mode 100644 frontend/src/component/common/dropdown-menu.jsx delete mode 100644 frontend/src/component/common/search-field.jsx create mode 100644 frontend/src/component/context/Context.module.scss create mode 100644 frontend/src/component/context/ContextList/ContextList.jsx rename frontend/src/component/context/{list-container.jsx => ContextList/index.jsx} (53%) create mode 100644 frontend/src/component/context/ContextList/styles.js delete mode 100644 frontend/src/component/context/list-component.jsx create mode 100644 frontend/src/component/feature/FeatureToggleList/FeatureToggleList.jsx create mode 100644 frontend/src/component/feature/FeatureToggleList/FeatureToggleListActions/FeatureToggleListActions.jsx create mode 100644 frontend/src/component/feature/FeatureToggleList/FeatureToggleListActions/index.jsx create mode 100644 frontend/src/component/feature/FeatureToggleList/FeatureToggleListActions/styles.js create mode 100644 frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItem.jsx create mode 100644 frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItemChip/FeatureToggleListItemChip.jsx rename frontend/src/component/feature/{list/feature-type-container.jsx => FeatureToggleList/FeatureToggleListItem/FeatureToggleListItemChip/index.jsx} (79%) create mode 100644 frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItemChip/styles.js create mode 100644 frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/index.jsx create mode 100644 frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/styles.js create mode 100644 frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap create mode 100644 frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/list-component-test.jsx.snap rename frontend/src/component/feature/{list => FeatureToggleList}/__tests__/feature-list-item-component-test.jsx (56%) create mode 100644 frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx rename frontend/src/component/feature/{list/list-container.jsx => FeatureToggleList/index.jsx} (92%) create mode 100644 frontend/src/component/feature/FeatureToggleList/loadingFeatures.js create mode 100644 frontend/src/component/feature/FeatureToggleList/styles.js create mode 100644 frontend/src/component/feature/FeatureView/FeatureView.jsx create mode 100644 frontend/src/component/feature/FeatureView/FeatureView.module.scss rename frontend/src/component/feature/{view/view-container.jsx => FeatureView/index.jsx} (88%) create mode 100644 frontend/src/component/feature/add-tag-dialog-component.module.scss delete mode 100644 frontend/src/component/feature/create/__tests__/__snapshots__/add-feature-component-test.jsx.snap delete mode 100644 frontend/src/component/feature/create/__tests__/add-feature-component-test.jsx create mode 100644 frontend/src/component/feature/create/add-feature-component.module.scss create mode 100644 frontend/src/component/feature/create/copy-feature-component.module.scss delete mode 100644 frontend/src/component/feature/list/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap delete mode 100644 frontend/src/component/feature/list/__tests__/__snapshots__/list-component-test.jsx.snap delete mode 100644 frontend/src/component/feature/list/__tests__/list-component-test.jsx delete mode 100644 frontend/src/component/feature/list/feature-type-component.jsx delete mode 100644 frontend/src/component/feature/list/list-component.jsx delete mode 100644 frontend/src/component/feature/list/list-item-component.jsx delete mode 100644 frontend/src/component/feature/list/list.module.scss delete mode 100644 frontend/src/component/feature/list/project-component.jsx create mode 100644 frontend/src/component/feature/status.module.scss create mode 100644 frontend/src/component/feature/strategy/AddStrategy/AddStrategy.jsx create mode 100644 frontend/src/component/feature/strategy/AddStrategy/AddStrategy.styles.js create mode 100644 frontend/src/component/feature/strategy/AddStrategy/AddStrategyCard/AddStrategyCard.jsx create mode 100644 frontend/src/component/feature/strategy/AddStrategy/AddStrategyCard/AddStrategyCard.styles.js create mode 100644 frontend/src/component/feature/strategy/AddStrategy/utils.js create mode 100644 frontend/src/component/feature/strategy/EditStrategyModal/EditStrategyModal.jsx create mode 100644 frontend/src/component/feature/strategy/EditStrategyModal/FlexibleStrategy.jsx rename frontend/src/component/feature/strategy/{ => EditStrategyModal}/default-strategy.jsx (100%) rename frontend/src/component/feature/strategy/{ => EditStrategyModal}/general-strategy.jsx (60%) create mode 100644 frontend/src/component/feature/strategy/EditStrategyModal/input-list.jsx create mode 100644 frontend/src/component/feature/strategy/EditStrategyModal/input-percentage.jsx rename frontend/src/component/feature/strategy/{ => EditStrategyModal}/loading-strategy.jsx (100%) rename frontend/src/component/feature/strategy/{ => EditStrategyModal}/strategy-input-props.js (88%) rename frontend/src/component/feature/strategy/{ => EditStrategyModal}/unknown-strategy.jsx (100%) rename frontend/src/component/feature/strategy/{ => EditStrategyModal}/user-with-id-strategy.jsx (100%) create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCard.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCard.styles.js create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/StrategyCardContent.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/StrategyCardContentCustom/StrategyCardContentCustom.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/StrategyCardContentDefault/StrategyCardContentDefault.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/StrategyCardContentFlexible/StrategyCardContentFlexible.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/StrategyCardContentGradRandom/StrategyCardContentGradRandom.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/StrategyCardContentList/StrategyCardContentList.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/StrategyCardContentRollout/StrategyCardContentRollout.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/common/StrategyCardConstraints/StrategyCardConstraints.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/common/StrategyCardConstraints/StrategyCardConstraints.styles.js create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/common/StrategyCardField/StrategyCardField.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/common/StrategyCardField/StrategyCardField.styles.js create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/common/StrategyCardList/StrategyCardList.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/common/StrategyCardList/StrategyCardList.styles.js create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/common/StrategyCardPercentage/StrageyCardPercentage.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardContent/common/StrategyCardPercentage/StrategyCardPercentage.styles.js create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardHeader/StrategyCardHeader.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyCard/StrategyCardHeader/StrategyCardHeader.styles.js rename frontend/src/component/feature/strategy/{strategy-configure-container.jsx => StrategyCard/index.jsx} (91%) create mode 100644 frontend/src/component/feature/strategy/StrategyConstraint/StrategyConstraintInput/StrategyConstraintInput.jsx rename frontend/src/component/feature/strategy/{constraint/strategy-constraint-input-container.jsx => StrategyConstraint/StrategyConstraintInput/index.jsx} (71%) create mode 100644 frontend/src/component/feature/strategy/StrategyConstraint/StrategyConstraintInputField/StrategyConstraintInputField.jsx create mode 100644 frontend/src/component/feature/strategy/StrategyConstraint/StrategyConstraintInputField/StrategyConstraintInputField.styles.js create mode 100644 frontend/src/component/feature/strategy/StrategyConstraint/StrategyConstraintInputField/index.jsx delete mode 100644 frontend/src/component/feature/strategy/__test__/.eslintrc delete mode 100644 frontend/src/component/feature/strategy/__test__/strategy-add-test.jsx delete mode 100644 frontend/src/component/feature/strategy/__test__/strategy-input-list-test.jsx delete mode 100644 frontend/src/component/feature/strategy/constraint/strategy-constraint-input-field.jsx delete mode 100644 frontend/src/component/feature/strategy/constraint/strategy-constraint-input.jsx delete mode 100644 frontend/src/component/feature/strategy/flexible-rollout-strategy-container.jsx delete mode 100644 frontend/src/component/feature/strategy/flexible-rollout-strategy.jsx delete mode 100644 frontend/src/component/feature/strategy/input-list.jsx delete mode 100644 frontend/src/component/feature/strategy/input-percentage.jsx delete mode 100644 frontend/src/component/feature/strategy/strategies-list-add-component.jsx delete mode 100644 frontend/src/component/feature/strategy/strategies-list-add-container.jsx delete mode 100644 frontend/src/component/feature/strategy/strategy-configure-component.jsx create mode 100644 frontend/src/component/feature/view/update-description-component.module.scss delete mode 100644 frontend/src/component/feature/view/view-component.jsx create mode 100644 frontend/src/component/menu/Footer/Footer.jsx create mode 100644 frontend/src/component/menu/Footer/Footer.module.scss create mode 100644 frontend/src/component/menu/Header/Header.jsx create mode 100644 frontend/src/component/menu/Header/index.jsx create mode 100644 frontend/src/component/menu/Header/styles.js create mode 100644 frontend/src/component/menu/drawer.module.scss delete mode 100644 frontend/src/component/menu/footer.jsx delete mode 100644 frontend/src/component/menu/header.jsx create mode 100644 frontend/src/component/project/Project.module.scss create mode 100644 frontend/src/component/project/ProjectList/ProjectList.jsx rename frontend/src/component/project/{list-container.jsx => ProjectList/index.jsx} (57%) create mode 100644 frontend/src/component/project/ProjectList/styles.js create mode 100644 frontend/src/component/strategies/StrategiesList/StrategiesList.jsx rename frontend/src/component/strategies/{list-container.jsx => StrategiesList/index.jsx} (80%) create mode 100644 frontend/src/component/strategies/StrategiesList/styles.js create mode 100644 frontend/src/component/strategies/form-strategy.jsx delete mode 100644 frontend/src/component/strategies/from-strategy.jsx delete mode 100644 frontend/src/component/strategies/list-component.jsx create mode 100644 frontend/src/component/strategies/toggles-link-list.jsx create mode 100644 frontend/src/component/tag-types/TagType.module.scss create mode 100644 frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx create mode 100644 frontend/src/component/tag-types/TagTypeList/index.jsx rename frontend/src/component/tag-types/{list-container.jsx => index.jsx} (72%) delete mode 100644 frontend/src/component/tag-types/list-component.jsx create mode 100644 frontend/src/component/tags/Tag.module.scss create mode 100644 frontend/src/component/tags/TagList/TagList.jsx create mode 100644 frontend/src/component/tags/TagList/TagList.styles.js create mode 100644 frontend/src/component/tags/TagList/index.jsx rename frontend/src/component/tags/{list-container.jsx => index.jsx} (93%) delete mode 100644 frontend/src/component/tags/list-component.jsx create mode 100644 frontend/src/component/user/PasswordAuth/PasswordAuth.jsx create mode 100644 frontend/src/component/user/PasswordAuth/PasswordAuth.styles.js create mode 100644 frontend/src/component/user/SimpleAuth/SimpleAuth.jsx create mode 100644 frontend/src/component/user/SimpleAuth/SimpleAuth.module.scss create mode 100644 frontend/src/component/user/SimpleAuth/index.jsx delete mode 100644 frontend/src/component/user/authentication-password-component.jsx delete mode 100644 frontend/src/component/user/authentication-simple-component.jsx create mode 100644 frontend/src/page/admin/api/styles.js create mode 100644 frontend/src/page/admin/users/UsersList/UsersList.jsx rename frontend/src/page/admin/users/{users-list-container.js => UsersList/index.js} (78%) create mode 100644 frontend/src/page/admin/users/del-user-component.jsx delete mode 100644 frontend/src/page/admin/users/users-list-component.jsx create mode 100644 frontend/src/store/api-calls/index.js create mode 100644 frontend/src/testIds.js create mode 100644 frontend/src/themes/main-theme.js create mode 100644 frontend/src/utils/strategy-names.js diff --git a/frontend/package.json b/frontend/package.json index 9db00def73..272589bef3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,13 +38,8 @@ "prepublish": "npm run build" }, "main": "./index.js", - "dependencies": {}, "devDependencies": { - "@material-ui/core": "^4.11.3", - "@material-ui/icons": "^4.11.2", "@material-ui/lab": "4.0.0-alpha.57", - "classnames": "^2.2.6", - "date-fns": "^2.17.0", "@babel/core": "^7.9.0", "@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-decorators": "^7.8.3", @@ -52,12 +47,17 @@ "@babel/plugin-transform-runtime": "^7.9.0", "@babel/preset-env": "^7.9.5", "@babel/preset-react": "^7.9.4", + "@material-ui/core": "^4.11.3", + "@material-ui/icons": "^4.11.2", + "@material-ui/styles": "^4.11.3", "array-move": "^2.2.1", "babel-eslint": "^10.1.0", "babel-jest": "^25.3.0", "babel-loader": "^8.1.0", + "classnames": "^2.2.6", "clean-webpack-plugin": "^3.0.0", "css-loader": "^2.1.1", + "date-fns": "^2.17.0", "debounce": "^1.2.0", "debug": "^4.1.1", "enzyme": "^3.9.0", @@ -72,7 +72,8 @@ "identity-obj-proxy": "^3.0.0", "immutable": "^3.8.1", "jest": "^26.6.3", - "lodash": "^4.17.20", + "lodash.clonedeep": "^4.5.0", + "lodash.flow": "^3.5.0", "mini-css-extract-plugin": "^0.9.0", "node-fetch": "^2.6.1", "node-sass": "^4.5.3", @@ -84,11 +85,8 @@ "react-dnd": "^11.1.3", "react-dnd-html5-backend": "^11.1.3", "react-dom": "^16.14.0", - "react-mdl": "^2.1.0", - "react-modal": "^3.1.13", "react-redux": "^7.2.0", "react-router-dom": "^5.1.2", - "react-select": "^3.1.0", "react-test-renderer": "^16.14.0", "react-timeago": "^4.4.0", "redux": "^4.0.5", @@ -100,6 +98,7 @@ "toolbox-loader": "0.0.3", "uglifyjs-webpack-plugin": "^2.2.0", "webpack": "^4.17.1", + "webpack-bundle-analyzer": "^4.4.0", "webpack-cli": "^3.1.0", "webpack-dev-server": "^3.11.2", "whatwg-fetch": "^3.4.1" @@ -121,5 +120,7 @@ "testPathIgnorePatterns": [ "/src/store/addons/__tests__/data.js" ] + }, + "dependencies": { } } diff --git a/frontend/src/__mocks__/react-mdl.js b/frontend/src/__mocks__/react-mdl.js deleted file mode 100644 index e525a380e7..0000000000 --- a/frontend/src/__mocks__/react-mdl.js +++ /dev/null @@ -1,37 +0,0 @@ -module.exports = { - Card: 'react-mdl-Card', - CardActions: 'react-mdl-CardActions', - CardTitle: 'react-mdl-CardTitle', - CardText: 'react-mdl-CardText', - CardMenu: 'react-mdl-CardMenu', - DataTable: 'react-mdl-DataTable', - Drawer: 'react-mdl-Drawer', - Cell: 'react-mdl-Cell', - Chip: 'react-mdl-Chip', - Grid: 'react-mdl-Grid', - Button: 'react-mdl-Button', - FABButton: 'react-mdl-FABButton', - Icon: 'react-mdl-Icon', - IconButton: 'react-mdl-IconButton', - List: 'react-mdl-List', - ListItem: 'react-mdl-ListItem', - ListItemContent: 'react-mdl-ListItemContent', - ListItemAction: 'react-mdl-ListItemAction', - Menu: 'react-mdl-Menu', - MenuItem: 'react-mdl-MenuItem', - Navigation: 'react-mdl-Navigation', - ProgressBar: 'react-mdl-ProgressBar', - Switch: 'react-mdl-Switch', - Tab: 'react-mdl-Tab', - Tabs: 'react-mdl-Tabs', - TableHeader: 'react-mdl-TableHeader', - Textfield: 'react-mdl-Textfield', - FooterDropDownSection: 'react-mdl-FooterDropDownSection', - FooterSection: 'react-mdl-FooterSection', - FooterLinkList: 'react-mdl-FooterLinkList', - Tooltip: 'react-mdl-Tooltip', - Dialog: 'react-mdl-Dialog', - DialogTitle: 'react-mdl-DialogTitle', - DialogContent: 'react-mdl-DialogContent', - DialogActions: 'react-mdl-DialogActions', -}; diff --git a/frontend/src/__mocks__/react-modal.js b/frontend/src/__mocks__/react-modal.js deleted file mode 100644 index cabed1fb38..0000000000 --- a/frontend/src/__mocks__/react-modal.js +++ /dev/null @@ -1,12 +0,0 @@ -// __mocks__/react-modal.js -const Modal = require('react-modal'); - -const oldFn = Modal.setAppElement; -Modal.setAppElement = element => { - if (element === '#app') { - // otherwise it will throw aria warnings. - return oldFn(document.createElement('div')); - } - oldFn(element); -}; -module.exports = Modal; diff --git a/frontend/src/app.css b/frontend/src/app.css index fe63346c64..e2ddd4ad8c 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,17 +1,74 @@ +* { + box-sizing: border-box; +} +html { + height: 100%; + overflow: auto; +} +body { + height: 100%; +} -html { height: 100%; overflow:auto; } -body { height: 100%; } +.skeleton { + position: relative; + overflow: hidden; + background-color: #e2e8f0; + z-index: 9999; + box-shadow: none; +} + +.skeleton::before { + background-color: #e2e8f0; + content: ""; + position: absolute; + top: 0; + right: 0; + content-visibility: hidden; + bottom: 0; + z-index: 5000; + left: 0; +} + +.skeleton::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + transform: translateX(-100%); + background-image: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0, + rgba(255, 255, 255, 0.2) 100%, + rgba(255, 255, 255, 0.5) 100%, + rgba(255, 255, 255, 0) + ); + animation: shimmer 3s infinite; + content: ""; + z-index: 5001; +} + +@keyframes shimmer { + 100% { + transform: translateX(100%); + } +} :root { /* FONT SIZE */ --h1-size: 1.25rem; - --p-size: 1.1rem; + --p-size: 1rem; + --caption-size: 0.9rem; /* PADDING */ --card-padding: 2rem; --card-padding-x: 2rem; --card-padding-y: 2rem; + --card-header-padding: 1rem 2rem; + --drawer-padding: 1rem 1.5rem; + --page-padding: 2rem 0; + --list-header-padding: 1rem; /* MARGIN */ --card-margin-y: 1rem; @@ -21,6 +78,25 @@ body { height: 100%; } --success: #3bd86e; --danger: #d95e5e; --warning: #d67c3d; + --drawer-link-active: #000; + --drawer-link-active-bg: #f1f1f1; + --drawer-link-inactive: #424242; + --primary: #607d8b; + + /* WIDTHS */ + --drawer-width: 250px; + --dropdownMenuWidth: 200px; + + /* BOX SHADOWS */ + --chip-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); + + /* BORDERS */ + --default-border: 1px solid #f1f1f1; +} + +body { + font-size: 16px; } h1, @@ -29,3 +105,22 @@ h2 { margin: 0; line-height: 24px; } + +p { + margin: 0; + padding: 0; +} + +#app { + height: 100%; +} + +.MuiCardHeader-title { + font-size: var(--p-size); +} + +@media screen and (max-width: 1024px) { + :root { + --drawer-padding: 0.75rem 1.25rem; + } +} diff --git a/frontend/src/common.styles.js b/frontend/src/common.styles.js new file mode 100644 index 0000000000..8d74015308 --- /dev/null +++ b/frontend/src/common.styles.js @@ -0,0 +1,35 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useCommonStyles = makeStyles(theme => ({ + contentSpacingY: { + '& > *': { + margin: '0.6rem 0', + }, + }, + contentSpacingX: { + '& > *': { + margin: '0 0.8rem', + }, + }, + divider: { + margin: '1rem 0', + backgroundColor: theme.palette.division.main, + height: '3px', + }, + bold: { + fontWeight: 'bold', + }, + flexRow: { + display: 'flex', + }, + flexColumn: { + display: 'flex', + flexDirection: 'column', + }, + flexWrap: { + flexWrap: 'wrap', + }, + textCenter: { + textAlign: 'center', + }, +})); diff --git a/frontend/src/component/reporting/report-card.jsx b/frontend/src/component/Reporting/ReportCard/ReportCard.jsx similarity index 93% rename from frontend/src/component/reporting/report-card.jsx rename to frontend/src/component/Reporting/ReportCard/ReportCard.jsx index df897f500d..bd1952f71a 100644 --- a/frontend/src/component/reporting/report-card.jsx +++ b/frontend/src/component/Reporting/ReportCard/ReportCard.jsx @@ -1,15 +1,15 @@ import React from 'react'; import classnames from 'classnames'; -import { Card } from 'react-mdl'; +import { Paper } from '@material-ui/core'; import PropTypes from 'prop-types'; import CheckIcon from '@material-ui/icons/Check'; import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined'; -import ConditionallyRender from '../common/conditionally-render'; +import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; -import { isFeatureExpired } from './utils'; +import { isFeatureExpired } from '../utils'; -import styles from './reporting.module.scss'; +import styles from './ReportCard.module.scss'; const ReportCard = ({ features }) => { const getActiveToggles = () => { @@ -76,7 +76,7 @@ const ReportCard = ({ features }) => { ); return ( - +

Toggle report

@@ -113,7 +113,7 @@ const ReportCard = ({ features }) => {
-
+ ); }; diff --git a/frontend/src/component/Reporting/ReportCard/ReportCard.module.scss b/frontend/src/component/Reporting/ReportCard/ReportCard.module.scss new file mode 100644 index 0000000000..7f76b5dd12 --- /dev/null +++ b/frontend/src/component/Reporting/ReportCard/ReportCard.module.scss @@ -0,0 +1,85 @@ +.card { + width: 100%; + padding: var(--card-padding); + margin: var(--card-margin-y) 0; +} + +.header { + font-size: var(--h1-size); + font-weight: 500; + margin: 0 0 0.5rem 0; +} + +.reportCardContainer { + display: flex; + justify-content: space-between; +} + +.reportCardHealthInnerContainer { + display: flex; + align-items: center; + justify-content: center; + align-items: center; + height: 80%; +} + +.reportCardHealthRating { + font-size: 2rem; + font-weight: bold; + color: var(--success); +} + +.reportCardList { + list-style-type: none; + margin: 0; + padding: 0; +} + +.reportCardList li { + display: flex; + align-items: center; + margin: 0.5rem 0; +} + +.reportCardList li span { + margin: 0; + padding: 0; + margin-left: 0.5rem; + font-size: var(--p-size); +} + +.check, +.danger { + margin-right: 5px; +} + +.check { + color: var(--success); +} + +.danger { + color: var(--danger); +} + +.reportCardActionContainer { + display: flex; + justify-content: center; + flex-direction: column; +} + +.reportCardActionText { + max-width: 300px; + font-size: var(--p-size); +} + +.reportCardBtn { + background-color: #f2f2f2; +} + +.healthDanger { + color: var(--danger); +} + +.healthWarning { + color: var(--warning); +} diff --git a/frontend/src/component/reporting/report-card-container.jsx b/frontend/src/component/Reporting/ReportCard/ReportCardContainer.jsx similarity index 83% rename from frontend/src/component/reporting/report-card-container.jsx rename to frontend/src/component/Reporting/ReportCard/ReportCardContainer.jsx index a4149ca51e..fb1a1ccda3 100644 --- a/frontend/src/component/reporting/report-card-container.jsx +++ b/frontend/src/component/Reporting/ReportCard/ReportCardContainer.jsx @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; -import ReportCard from './report-card'; -import { filterByProject } from './utils'; +import ReportCard from './ReportCard'; +import { filterByProject } from '../utils'; const mapStateToProps = (state, ownProps) => { const features = state.features.toJS(); diff --git a/frontend/src/component/reporting/report-toggle-list.jsx b/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.jsx similarity index 68% rename from frontend/src/component/reporting/report-toggle-list.jsx rename to frontend/src/component/Reporting/ReportToggleList/ReportToggleList.jsx index 6057e64c3a..da6f1d6239 100644 --- a/frontend/src/component/reporting/report-toggle-list.jsx +++ b/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.jsx @@ -1,18 +1,17 @@ import React, { useState, useEffect } from 'react'; -import classnames from 'classnames'; -import { Card, Menu, MenuItem } from 'react-mdl'; +import { Paper, MenuItem } from '@material-ui/core'; import PropTypes from 'prop-types'; -import ReportToggleListItem from './report-toggle-list-item'; -import ReportToggleListHeader from './report-toggle-list-header'; -import ConditionallyRender from '../common/conditionally-render'; +import ReportToggleListItem from './ReportToggleListItem/ReportToggleListItem'; +import ReportToggleListHeader from './ReportToggleListHeader/ReportToggleListHeader'; +import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; +import DropdownMenu from '../../common/dropdown-menu'; -import { getObjectProperties, getCheckedState, applyCheckedToFeatures } from './utils'; +import { getObjectProperties, getCheckedState, applyCheckedToFeatures } from '../utils'; -import useSort from './useSort'; +import useSort from '../useSort'; -import styles from './reporting.module.scss'; -import { DropdownButton } from '../common'; +import styles from './ReportToggleList.module.scss'; /* FLAG TO TOGGLE UNFINISHED BULK ACTIONS FEATURE */ const BULK_ACTIONS_ON = false; @@ -47,26 +46,20 @@ const ReportToggleList = ({ features, selectedProject }) => { )); const renderBulkActionsMenu = () => ( - - - console.log("Hi")} - style={{ width: '168px' }} - > - Mark toggles as stale - Delete toggles - - + ( + <> + Mark toggles as stale + Delete toggles + + )} + /> ); return ( - +

Overview

@@ -83,7 +76,7 @@ const ReportToggleList = ({ features, selectedProject }) => { {renderListRows()}
-
+ ); }; diff --git a/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.module.scss b/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.module.scss new file mode 100644 index 0000000000..b86dfe94d4 --- /dev/null +++ b/frontend/src/component/Reporting/ReportToggleList/ReportToggleList.module.scss @@ -0,0 +1,71 @@ +.reportToggleList { + width: 100%; + margin: var(--card-margin-y) 0; +} + +.bulkAction { + background-color: #f2f2f2; + font-size: var(--p-size); +} + +.sortIcon { + margin-left: 8px; +} + +.reportToggleListHeader { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #f1f1f1; + padding: 1rem var(--card-padding-x); +} + +.reportToggleListInnerContainer { + padding: var(--card-padding); +} + +.reportToggleListHeading { + font-size: var(--h1-size); + margin: 0; + font-weight: 500; +} + +.reportingToggleTable { + width: 100%; + border-spacing: 0 0.8rem; +} + +.reportingToggleTable th { + text-align: left; +} + +.expired { + color: var(--danger); +} + +.active { + color: var(--success); +} + +.stale { + color: var(--danger); +} + +.reportStatus { + display: flex; + align-items: center; +} + +.reportIcon { + font-size: 1.5rem; + margin-right: 5px; +} + +.tableRow { + cursor: pointer; +} + +.checkbox { + margin: 0; + padding: 0; +} diff --git a/frontend/src/component/reporting/report-toggle-list-container.jsx b/frontend/src/component/Reporting/ReportToggleList/ReportToggleListContainer.jsx similarity index 82% rename from frontend/src/component/reporting/report-toggle-list-container.jsx rename to frontend/src/component/Reporting/ReportToggleList/ReportToggleListContainer.jsx index f50366a785..a3ead1366f 100644 --- a/frontend/src/component/reporting/report-toggle-list-container.jsx +++ b/frontend/src/component/Reporting/ReportToggleList/ReportToggleListContainer.jsx @@ -1,8 +1,8 @@ import { connect } from 'react-redux'; -import { filterByProject } from './utils'; +import { filterByProject } from '../utils'; -import ReportToggleList from './report-toggle-list'; +import ReportToggleList from './ReportToggleList'; const mapStateToProps = (state, ownProps) => { const features = state.features.toJS(); diff --git a/frontend/src/component/reporting/report-toggle-list-header.jsx b/frontend/src/component/Reporting/ReportToggleList/ReportToggleListHeader/ReportToggleListHeader.jsx similarity index 82% rename from frontend/src/component/reporting/report-toggle-list-header.jsx rename to frontend/src/component/Reporting/ReportToggleList/ReportToggleListHeader/ReportToggleListHeader.jsx index cbdd4eb24d..9ba5e8fbbf 100644 --- a/frontend/src/component/reporting/report-toggle-list-header.jsx +++ b/frontend/src/component/Reporting/ReportToggleList/ReportToggleListHeader/ReportToggleListHeader.jsx @@ -1,13 +1,13 @@ import React from 'react'; -import { Checkbox } from 'react-mdl'; +import { Checkbox } from '@material-ui/core'; import UnfoldMoreOutlinedIcon from '@material-ui/icons/UnfoldMoreOutlined'; import PropTypes from 'prop-types'; -import ConditionallyRender from '../common/conditionally-render'; +import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; -import { NAME, LAST_SEEN, CREATED, EXPIRED, STATUS, REPORT } from './constants'; +import { NAME, LAST_SEEN, CREATED, EXPIRED, STATUS, REPORT } from '../../constants'; -import styles from './reporting.module.scss'; +import styles from '../ReportToggleList.module.scss'; const ReportToggleListHeader = ({ handleCheckAll, checkAll, setSortData, bulkActionsOn }) => { const handleSort = type => { @@ -24,7 +24,12 @@ const ReportToggleListHeader = ({ handleCheckAll, checkAll, setSortData, bulkAct condition={bulkActionsOn} show={ - + } /> diff --git a/frontend/src/component/reporting/report-toggle-list-item.jsx b/frontend/src/component/Reporting/ReportToggleList/ReportToggleListItem/ReportToggleListItem.jsx similarity index 87% rename from frontend/src/component/reporting/report-toggle-list-item.jsx rename to frontend/src/component/Reporting/ReportToggleList/ReportToggleListItem/ReportToggleListItem.jsx index 12ecf11069..8d3d561eb6 100644 --- a/frontend/src/component/reporting/report-toggle-list-item.jsx +++ b/frontend/src/component/Reporting/ReportToggleList/ReportToggleListItem/ReportToggleListItem.jsx @@ -3,15 +3,15 @@ import classnames from 'classnames'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; -import { Checkbox } from 'react-mdl'; +import { Checkbox } from '@material-ui/core'; import CheckIcon from '@material-ui/icons/Check'; import ReportProblemOutlinedIcon from '@material-ui/icons/ReportProblemOutlined'; -import ConditionallyRender from '../common/conditionally-render'; +import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; -import { pluralize, getDates, expired, toggleExpiryByTypeMap, getDiffInDays } from './utils'; -import { KILLSWITCH, PERMISSION } from './constants'; +import { pluralize, getDates, expired, toggleExpiryByTypeMap, getDiffInDays } from '../../utils'; +import { KILLSWITCH, PERMISSION } from '../../constants'; -import styles from './reporting.module.scss'; +import styles from '../ReportToggleList.module.scss'; const ReportToggleListItem = ({ name, stale, lastSeenAt, createdAt, type, checked, bulkActionsOn, setFeatures }) => { const nameMatches = feature => feature.name === name; @@ -107,7 +107,12 @@ const ReportToggleListItem = ({ name, stale, lastSeenAt, createdAt, type, checke condition={bulkActionsOn} show={ - + } /> diff --git a/frontend/src/component/reporting/reporting.jsx b/frontend/src/component/Reporting/Reporting.jsx similarity index 79% rename from frontend/src/component/reporting/reporting.jsx rename to frontend/src/component/Reporting/Reporting.jsx index 1cbe25e4b9..572f7d90a5 100644 --- a/frontend/src/component/reporting/reporting.jsx +++ b/frontend/src/component/Reporting/Reporting.jsx @@ -3,14 +3,15 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import Select from '../common/select'; -import ReportCardContainer from './report-card-container'; -import ReportToggleListContainer from './report-toggle-list-container'; +import ReportCardContainer from './ReportCard/ReportCardContainer'; +import ReportToggleListContainer from './ReportToggleList/ReportToggleListContainer'; -import ConditionallyRender from '../common/conditionally-render'; +import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; import { formatProjectOptions } from './utils'; +import { REPORTING_SELECT_ID } from '../../testIds'; -import styles from './reporting.module.scss'; +import styles from './Reporting.module.scss'; const Reporting = ({ fetchFeatureToggles, projects }) => { const [projectOptions, setProjectOptions] = useState([{ key: 'default', label: 'Default' }]); @@ -40,11 +41,13 @@ const Reporting = ({ fetchFeatureToggles, projects }) => { name="project" className={styles.select} options={projectOptions} - value={setSelectedProject.label} + value={selectedProject} onChange={onChange} + inputProps={{ ['data-test']: REPORTING_SELECT_ID }} /> ); + const multipleProjects = projects.length > 1; return ( diff --git a/frontend/src/component/reporting/reporting.module.scss b/frontend/src/component/Reporting/Reporting.module.scss similarity index 98% rename from frontend/src/component/reporting/reporting.module.scss rename to frontend/src/component/Reporting/Reporting.module.scss index 618deccb6f..8633270338 100644 --- a/frontend/src/component/reporting/reporting.module.scss +++ b/frontend/src/component/Reporting/Reporting.module.scss @@ -125,7 +125,7 @@ display: flex; justify-content: space-between; align-items: center; - border-bottom: 1px solid #f1f1f1; + border-bottom: var(--default-border); padding: 1rem var(--card-padding-x); } diff --git a/frontend/src/component/reporting/reporting-container.jsx b/frontend/src/component/Reporting/ReportingContainer.jsx similarity index 90% rename from frontend/src/component/reporting/reporting-container.jsx rename to frontend/src/component/Reporting/ReportingContainer.jsx index e7aed8fa22..5e84adb3a8 100644 --- a/frontend/src/component/reporting/reporting-container.jsx +++ b/frontend/src/component/Reporting/ReportingContainer.jsx @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { fetchFeatureToggles } from '../../store/feature-toggle/actions'; -import Reporting from './reporting'; +import Reporting from './Reporting.jsx'; const mapStateToProps = state => ({ projects: state.projects.toJS(), diff --git a/frontend/src/component/reporting/__tests__/reporting-test.js b/frontend/src/component/Reporting/__tests__/reporting-test.js similarity index 63% rename from frontend/src/component/reporting/__tests__/reporting-test.js rename to frontend/src/component/Reporting/__tests__/reporting-test.js index 597b710818..adbd4a81ed 100644 --- a/frontend/src/component/reporting/__tests__/reporting-test.js +++ b/frontend/src/component/Reporting/__tests__/reporting-test.js @@ -5,7 +5,8 @@ import { HashRouter } from 'react-router-dom'; import { createStore } from 'redux'; import { mount } from 'enzyme/build'; -import Reporting from '../reporting'; +import Reporting from '../Reporting'; +import { REPORTING_SELECT_ID } from '../../../testIds'; import { testProjects, testFeatures } from '../testData'; @@ -15,13 +16,6 @@ const mockStore = { }; const mockReducer = state => state; -jest.mock('react-mdl', () => ({ - Checkbox: jest.fn().mockImplementation(({ children }) => children), - Card: jest.fn().mockImplementation(({ children }) => children), - Menu: jest.fn().mockImplementation(({ children }) => children), - MenuItem: jest.fn().mockImplementation(({ children }) => children), -})); - test('changing projects renders only toggles from that project', () => { const wrapper = mount( @@ -31,9 +25,7 @@ test('changing projects renders only toggles from that project', () => { ); - const select = wrapper.find('.mdl-textfield__input').first(); - expect(select.contains()).toBe(true); - expect(select.contains()).toBe(true); + const select = wrapper.find(`input[data-test="${REPORTING_SELECT_ID}"][value="default"]`).first(); let list = wrapper.find('tr'); /* Length of projects belonging to project (3) + header row (1) */ diff --git a/frontend/src/component/reporting/__tests__/sorting-test.js b/frontend/src/component/Reporting/__tests__/sorting-test.js similarity index 100% rename from frontend/src/component/reporting/__tests__/sorting-test.js rename to frontend/src/component/Reporting/__tests__/sorting-test.js diff --git a/frontend/src/component/reporting/constants.js b/frontend/src/component/Reporting/constants.js similarity index 100% rename from frontend/src/component/reporting/constants.js rename to frontend/src/component/Reporting/constants.js diff --git a/frontend/src/component/reporting/testData.js b/frontend/src/component/Reporting/testData.js similarity index 100% rename from frontend/src/component/reporting/testData.js rename to frontend/src/component/Reporting/testData.js diff --git a/frontend/src/component/reporting/useSort.js b/frontend/src/component/Reporting/useSort.js similarity index 100% rename from frontend/src/component/reporting/useSort.js rename to frontend/src/component/Reporting/useSort.js diff --git a/frontend/src/component/reporting/utils.js b/frontend/src/component/Reporting/utils.js similarity index 100% rename from frontend/src/component/reporting/utils.js rename to frontend/src/component/Reporting/utils.js diff --git a/frontend/src/component/addons/AddonList/AddonList.jsx b/frontend/src/component/addons/AddonList/AddonList.jsx new file mode 100644 index 0000000000..63911601e4 --- /dev/null +++ b/frontend/src/component/addons/AddonList/AddonList.jsx @@ -0,0 +1,70 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import ConfiguredAddons from './ConfiguredAddons'; +import AvailableAddons from './AvailableAddons'; +import { Avatar, Icon } from '@material-ui/core'; +import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; + +const style = { + width: '40px', + height: '40px', + marginRight: '16px', + float: 'left', +}; + +const getIcon = name => { + switch (name) { + case 'slack': + return ; + case 'jira-comment': + return ; + case 'webhook': + return ; + default: + return ( + + device_hub + + ); + } +}; + +const AddonList = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, history, hasPermission }) => { + useEffect(() => { + if (addons.length === 0) { + fetchAddons(); + } + }, []); + + return ( + <> + 0} + show={ + + } + /> + +
+ + + ); +}; + +AddonList.propTypes = { + addons: PropTypes.array.isRequired, + providers: PropTypes.array.isRequired, + fetchAddons: PropTypes.func.isRequired, + removeAddon: PropTypes.func.isRequired, + toggleAddon: PropTypes.func.isRequired, + history: PropTypes.object.isRequired, + hasPermission: PropTypes.func.isRequired, +}; + +export default AddonList; diff --git a/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx b/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx new file mode 100644 index 0000000000..ad75d2d9a6 --- /dev/null +++ b/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import PageContent from '../../../common/PageContent/PageContent'; +import { Button, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText } from '@material-ui/core'; +import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; +import { CREATE_ADDON } from '../../../../permissions'; +import PropTypes from 'prop-types'; + +const AvailableAddons = ({ providers, getIcon, hasPermission, history }) => { + const renderProvider = provider => ( + + {getIcon(provider.name)} + + + history.push(`/addons/create/${provider.name}`)} + title="Configure" + > + Configure + + } + /> + + + ); + return ( + + {providers.map(provider => renderProvider(provider))} + + ); +}; + +AvailableAddons.propTypes = { + providers: PropTypes.array.isRequired, + getIcon: PropTypes.func.isRequired, + hasPermission: PropTypes.func.isRequired, + history: PropTypes.object.isRequired, +}; + +export default AvailableAddons; diff --git a/frontend/src/component/addons/AddonList/AvailableAddons/index.jsx b/frontend/src/component/addons/AddonList/AvailableAddons/index.jsx new file mode 100644 index 0000000000..00f3612923 --- /dev/null +++ b/frontend/src/component/addons/AddonList/AvailableAddons/index.jsx @@ -0,0 +1,3 @@ +import AvailableAddons from './AvailableAddons'; + +export default AvailableAddons; diff --git a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.jsx b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.jsx new file mode 100644 index 0000000000..a6d56ee09b --- /dev/null +++ b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.jsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { + Icon, + IconButton, + List, + ListItem, + ListItemAvatar, + ListItemSecondaryAction, + ListItemText, +} from '@material-ui/core'; +import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; +import { DELETE_ADDON, UPDATE_ADDON } from '../../../../permissions'; +import { Link } from 'react-router-dom'; +import PageContent from '../../../common/PageContent/PageContent'; +import PropTypes from 'prop-types'; + +const ConfiguredAddons = ({ addons, hasPermission, removeAddon, getIcon, toggleAddon }) => { + const onRemoveAddon = addon => () => removeAddon(addon); + const renderAddon = addon => ( + + {getIcon(addon.provider)} + + + {addon.provider} + + } + elseShow={{addon.provider}} + /> + {addon.enabled ? null : (Disabled)} + + } + secondary={addon.description} + /> + + toggleAddon(addon)} + > + {addon.enabled ? 'visibility' : 'visibility_off'} + + } + /> + + delete + + } + /> + + + ); + return ( + + {addons.map(addon => renderAddon(addon))} + + ); +}; +ConfiguredAddons.propTypes = { + addons: PropTypes.array.isRequired, + hasPermission: PropTypes.func.isRequired, + removeAddon: PropTypes.func.isRequired, + toggleAddon: PropTypes.func.isRequired, + getIcon: PropTypes.func.isRequired, +}; + +export default ConfiguredAddons; diff --git a/frontend/src/component/addons/AddonList/ConfiguredAddons/index.jsx b/frontend/src/component/addons/AddonList/ConfiguredAddons/index.jsx new file mode 100644 index 0000000000..8a8cf00375 --- /dev/null +++ b/frontend/src/component/addons/AddonList/ConfiguredAddons/index.jsx @@ -0,0 +1,3 @@ +import ConfiguredAddons from './ConfiguredAddons'; + +export default ConfiguredAddons; diff --git a/frontend/src/component/addons/AddonList/index.js b/frontend/src/component/addons/AddonList/index.js new file mode 100644 index 0000000000..be5a97ab70 --- /dev/null +++ b/frontend/src/component/addons/AddonList/index.js @@ -0,0 +1,3 @@ +import AddonListComponent from './AddonList'; + +export default AddonListComponent; diff --git a/frontend/src/component/addons/form-addon-component.jsx b/frontend/src/component/addons/form-addon-component.jsx index 4570160cdb..f696c1fc10 100644 --- a/frontend/src/component/addons/form-addon-component.jsx +++ b/frontend/src/component/addons/form-addon-component.jsx @@ -1,12 +1,15 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Textfield, Card, CardTitle, CardText, CardActions, Switch, Grid, Cell } from 'react-mdl'; +import { TextField, FormControlLabel, Switch } from '@material-ui/core'; import { FormButtons, styles as commonStyles } from '../common'; import { trim } from '../common/util'; import AddonParameters from './form-addon-parameters'; import AddonEvents from './form-addon-events'; -import { cloneDeep } from 'lodash'; +import cloneDeep from 'lodash.clonedeep'; + +import styles from './form-addon-component.module.scss'; +import PageContent from '../common/PageContent/PageContent'; const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit }) => { const [config, setConfig] = useState(addon); @@ -98,49 +101,46 @@ const AddonFormComponent = ({ editMode, provider, addon, fetch, cancel, submit } const { name, description, documentationUrl = 'https://unleash.github.io/docs/addons' } = provider ? provider : {}; return ( - - - Configure {name} - - + +
{description}  Read more

{errors.general}

- +
-
- - - - - - - {config.enabled ? 'Enabled' : 'Disabled'} - - - - - + + } + label={config.enabled ? 'Enabled' : 'Disabled'} + /> +
+
+
-
+
-
+
- +
- +
- + ); }; diff --git a/frontend/src/component/addons/form-addon-component.module.scss b/frontend/src/component/addons/form-addon-component.module.scss new file mode 100644 index 0000000000..6145c6cb6e --- /dev/null +++ b/frontend/src/component/addons/form-addon-component.module.scss @@ -0,0 +1,17 @@ +.nameInput { + margin-right: 1.5rem; +} + +.formContainer { + margin-bottom: 1.5rem; + max-width: 350px; +} + +.formSection { + padding: 10px 28px; +} + +.header { + font-size: var(--h1-size); + padding: var(--card-header-padding); +} diff --git a/frontend/src/component/addons/form-addon-container.js b/frontend/src/component/addons/form-addon-container.js index 8631f5d4a6..b2a7c721dc 100644 --- a/frontend/src/component/addons/form-addon-container.js +++ b/frontend/src/component/addons/form-addon-container.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import FormComponent from './form-addon-component'; import { updateAddon, createAddon, fetchAddons } from '../../store/addons/actions'; -import { cloneDeep } from 'lodash'; +import cloneDeep from 'lodash.clonedeep'; // Required for to fill the initial form. const DEFAULT_DATA = { diff --git a/frontend/src/component/addons/form-addon-events.jsx b/frontend/src/component/addons/form-addon-events.jsx index f4790432d1..1b6f7518d3 100644 --- a/frontend/src/component/addons/form-addon-events.jsx +++ b/frontend/src/component/addons/form-addon-events.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Checkbox, Grid, Cell } from 'react-mdl'; +import { Grid, FormControlLabel, Checkbox } from '@material-ui/core'; import { styles as commonStyles } from '../common'; @@ -11,11 +11,14 @@ const AddonEvents = ({ provider, checkedEvents, setEventValue, error }) => {

Events

{error} - + {provider.events.map(e => ( - - - + + } + label={e} + /> + ))}
diff --git a/frontend/src/component/addons/form-addon-parameters.jsx b/frontend/src/component/addons/form-addon-parameters.jsx index 8f6065126c..edaef3a141 100644 --- a/frontend/src/component/addons/form-addon-parameters.jsx +++ b/frontend/src/component/addons/form-addon-parameters.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Textfield } from 'react-mdl'; +import { TextField } from '@material-ui/core'; const MASKED_VALUE = '*****'; @@ -18,23 +18,27 @@ const AddonParameter = ({ definition, config, errors, setParameterValue }) => { const value = config.parameters[definition.name] || ''; const type = resolveType(definition, value); const error = errors.parameters[definition.name]; - const descStyle = { fontSize: '0.8em', color: 'gray', marginTop: error ? '2px' : '-15px' }; return (
- -
{definition.description}
); }; @@ -54,8 +58,8 @@ const AddonParameters = ({ provider, config, errors, setParameterValue, editMode

Parameters

{editMode ? (

- Sensitive parameters will be masked with value "*****". If you don't change the value they - will not be updated when saving. + Sensitive parameters will be masked with value "***** + ". If you don't change the value they will not be updated when saving.

) : null} {provider.parameters.map(p => ( @@ -76,7 +80,7 @@ AddonParameters.propTypes = { config: PropTypes.object.isRequired, errors: PropTypes.object.isRequired, setParameterValue: PropTypes.func.isRequired, - editMode: PropTypes.bool.optional, + editMode: PropTypes.bool, }; export default AddonParameters; diff --git a/frontend/src/component/addons/list-container.jsx b/frontend/src/component/addons/index.jsx similarity index 94% rename from frontend/src/component/addons/list-container.jsx rename to frontend/src/component/addons/index.jsx index ed83e458e6..1a5c6d65b8 100644 --- a/frontend/src/component/addons/list-container.jsx +++ b/frontend/src/component/addons/index.jsx @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import AddonsListComponent from './list-component.jsx'; +import AddonsListComponent from './AddonList'; import { fetchAddons, removeAddon, updateAddon } from '../../store/addons/actions'; import { hasPermission } from '../../permissions'; diff --git a/frontend/src/component/addons/list-component.jsx b/frontend/src/component/addons/list-component.jsx deleted file mode 100644 index eb7c23c79c..0000000000 --- a/frontend/src/component/addons/list-component.jsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; - -import { List, ListItem, ListItemAction, IconButton, Card, Button } from 'react-mdl'; -import { HeaderTitle, styles as commonStyles } from '../common'; -import { CREATE_ADDON, DELETE_ADDON, UPDATE_ADDON } from '../../permissions'; - -const style = { width: '40px', height: '40px', marginRight: '16px', float: 'left' }; - -const getIcon = name => { - switch (name) { - case 'slack': - return ; - case 'jira-comment': - return ; - case 'webhook': - return ; - default: - return device_hub; - } -}; - -const AddonListComponent = ({ addons, providers, fetchAddons, removeAddon, toggleAddon, history, hasPermission }) => { - useEffect(() => { - if (addons.length === 0) { - fetchAddons(); - } - }, []); - - const onRemoveAddon = addon => () => removeAddon(addon); - - return ( -
- {addons.length > 0 ? ( - - - - {addons.map(addon => ( - - - {getIcon(addon.provider)} - - {hasPermission(UPDATE_ADDON) ? ( - - {addon.provider} - - ) : ( - {addon.provider} - )} - {addon.enabled ? null : (Disabled)} - - {addon.description} - - - {hasPermission(UPDATE_ADDON) ? ( - toggleAddon(addon)} - /> - ) : null} - {hasPermission(DELETE_ADDON) ? ( - - ) : null} - - - ))} - - - ) : null} -
- - - - {providers.map((provider, i) => ( - - - {getIcon(provider.name)} - - {provider.displayName}  - - {provider.description} - - - {hasPermission(CREATE_ADDON) ? ( - - ) : ( - '' - )} - - - ))} - - -
- ); -}; -AddonListComponent.propTypes = { - addons: PropTypes.array.isRequired, - providers: PropTypes.array.isRequired, - fetchAddons: PropTypes.func.isRequired, - removeAddon: PropTypes.func.isRequired, - toggleAddon: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, - hasPermission: PropTypes.func.isRequired, -}; - -export default AddonListComponent; diff --git a/frontend/src/component/api/__tests__/__snapshots__/show-api-details-component-test.jsx.snap b/frontend/src/component/api/__tests__/__snapshots__/show-api-details-component-test.jsx.snap index 2381c154fb..f2bd470bc2 100644 --- a/frontend/src/component/api/__tests__/__snapshots__/show-api-details-component-test.jsx.snap +++ b/frontend/src/component/api/__tests__/__snapshots__/show-api-details-component-test.jsx.snap @@ -1,68 +1,60 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders correctly with empty version 1`] = ` - +

+ Unleash +

- (test) + \`($ + test + )\`
- - - +

We are the best!
- - - -
+
`; exports[`renders correctly with ui-config 1`] = ` - +

+ Unleash 1.1.0 +

- (test) + \`($ + test + )\`
- - - +

We are the best!
- - - -
+
`; exports[`renders correctly without uiConfig 1`] = ` - - - - +

+ Unleash 1.1.0 +

+

- - -

- - - -
+ `; diff --git a/frontend/src/component/api/__tests__/show-api-details-component-test.jsx b/frontend/src/component/api/__tests__/show-api-details-component-test.jsx index a26fd5b7ee..993f263016 100644 --- a/frontend/src/component/api/__tests__/show-api-details-component-test.jsx +++ b/frontend/src/component/api/__tests__/show-api-details-component-test.jsx @@ -3,8 +3,6 @@ import React from 'react'; import ShowApiDetailsComponent from '../show-api-details-component'; import renderer from 'react-test-renderer'; -jest.mock('react-mdl'); - test('renders correctly with empty version', () => { const uiConfig = { name: 'Unleash', diff --git a/frontend/src/component/api/show-api-details-component.jsx b/frontend/src/component/api/show-api-details-component.jsx index 218b42354e..dc4c0bbbae 100644 --- a/frontend/src/component/api/show-api-details-component.jsx +++ b/frontend/src/component/api/show-api-details-component.jsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { FooterSection } from 'react-mdl'; +import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; class ShowApiDetailsComponent extends Component { static propTypes = { @@ -15,12 +15,12 @@ class ShowApiDetailsComponent extends Component { if (versionInfo) { if (versionInfo.current.enterprise) { versionStr = `${name} ${versionInfo.current.enterprise}`; - if (versionInfo.latest && !versionInfo.isLatest) { + if (Object.keys(versionInfo.latest).includes('enterprise') && !versionInfo.isLatest) { updateNotification = `Upgrade available - Latest Enterprise release: ${versionInfo.latest.enterprise}`; } } else { versionStr = `${name} ${versionInfo.current.oss}`; - if (versionInfo.latest && !versionInfo.isLatest) { + if (Object.keys(versionInfo.latest).includes('oss') && !versionInfo.isLatest) { updateNotification = `Upgrade available - Latest OSS release: ${versionInfo.latest.oss}`; } } @@ -29,15 +29,17 @@ class ShowApiDetailsComponent extends Component { versionStr = `${name} ${version}`; } return ( - - {environment ? `(${environment})` : ''} +
+

{`${versionStr}`}

+ `(${environment})`
} /> +
+ {updateNotification}`} />
- {updateNotification ? `${updateNotification}` : ''}
{slogan}
- {instanceId ? `${instanceId}` : ''} - + {`${instanceId}`}} /> +
); } } diff --git a/frontend/src/component/app.jsx b/frontend/src/component/app.jsx index 2fd7be76b0..76c775b5a8 100644 --- a/frontend/src/component/app.jsx +++ b/frontend/src/component/app.jsx @@ -4,11 +4,12 @@ import PropTypes from 'prop-types'; import { Route, Redirect, Switch } from 'react-router-dom'; import Features from '../page/features'; -import { routes } from './menu/routes'; -import styles from './styles.module.scss'; import AuthenticationContainer from './user/authentication-container'; import MainLayout from './layout/main'; +import { routes } from './menu/routes'; + +import styles from './styles.module.scss'; class App extends PureComponent { static propTypes = { location: PropTypes.object.isRequired, diff --git a/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap index fcbf7276f8..623d1f4e8b 100644 --- a/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap +++ b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap @@ -5,409 +5,615 @@ exports[`renders correctly if no application 1`] = `

Loading...

- +
+
+
+
`; exports[`renders correctly with permissions 1`] = ` - - - -   - test-app - - -

- app description -

-

- Created: - - Invalid Date - -

-
- - - - - -
- - - - Delete - - -
- - - Details - - - Edit - - +

+ +
+ + apps + +
+ test-app +
+

+
+
+ + + + link + + + + +
+
- - -
- Toggles -
-
- - +

+ app description +

+

+ Created: + + Invalid Date + +

+ +
+
+
- - +
+ +
+
+
+
+
+ + +
+ + `; exports[`renders correctly without permission 1`] = ` - - - -   - test-app - - -

- app description -

-

- Created: - - Invalid Date - -

-
- - - - - - - - -
- Toggles -
-
- - +

- - - - } - subtitle="this is A toggle" - > - - ToggleA - - - - - - ToggleB - - - - - -
- Implemented strategies -
-
- - - - - StrategyA - - - - - - StrategyB - - - -
- -
- 1 - Instances registered -
-
- - - - 123.123.123.123 - last seen at - - 02/23/2017, 03:56:49 PM - - + - instance-1 - - (4.0) - - - -
- - +
+ + apps + +
+ test-app + +

+ + + + +
+
+

+ app description +

+

+ Created: + + Invalid Date + +

+
+
+ `; diff --git a/frontend/src/component/application/__tests__/application-edit-component-test.js b/frontend/src/component/application/__tests__/application-edit-component-test.js index cd3c82b968..c8935c57b7 100644 --- a/frontend/src/component/application/__tests__/application-edit-component-test.js +++ b/frontend/src/component/application/__tests__/application-edit-component-test.js @@ -1,11 +1,11 @@ import React from 'react'; +import { ThemeProvider } from '@material-ui/core'; import ClientApplications from '../application-edit-component'; import renderer from 'react-test-renderer'; import { MemoryRouter } from 'react-router-dom'; import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../../permissions'; - -jest.mock('react-mdl'); +import theme from '../../../themes/main-theme'; test('renders correctly if no application', () => { const tree = renderer @@ -27,51 +27,53 @@ test('renders correctly without permission', () => { const tree = renderer .create( - Promise.resolve({})} - storeApplicationMetaData={jest.fn()} - deleteApplication={jest.fn()} - history={{}} - application={{ - appName: 'test-app', - instances: [ - { - instanceId: 'instance-1', - clientIp: '123.123.123.123', - lastSeen: '2017-02-23T15:56:49', - sdkVersion: '4.0', - }, - ], - strategies: [ - { - name: 'StrategyA', - description: 'A description', - }, - { - name: 'StrategyB', - description: 'B description', - notFound: true, - }, - ], - seenToggles: [ - { - name: 'ToggleA', - description: 'this is A toggle', - enabled: true, - }, - { - name: 'ToggleB', - description: 'this is B toggle', - enabled: false, - notFound: true, - }, - ], - url: 'http://example.org', - description: 'app description', - }} - location={{ locale: 'en-GB' }} - hasPermission={() => false} - /> + + Promise.resolve({})} + storeApplicationMetaData={jest.fn()} + deleteApplication={jest.fn()} + history={{}} + application={{ + appName: 'test-app', + instances: [ + { + instanceId: 'instance-1', + clientIp: '123.123.123.123', + lastSeen: '2017-02-23T15:56:49', + sdkVersion: '4.0', + }, + ], + strategies: [ + { + name: 'StrategyA', + description: 'A description', + }, + { + name: 'StrategyB', + description: 'B description', + notFound: true, + }, + ], + seenToggles: [ + { + name: 'ToggleA', + description: 'this is A toggle', + enabled: true, + }, + { + name: 'ToggleB', + description: 'this is B toggle', + enabled: false, + notFound: true, + }, + ], + url: 'http://example.org', + description: 'app description', + }} + location={{ locale: 'en-GB' }} + hasPermission={() => false} + /> + ) .toJSON(); @@ -83,53 +85,55 @@ test('renders correctly with permissions', () => { const tree = renderer .create( - Promise.resolve({})} - storeApplicationMetaData={jest.fn()} - history={{}} - deleteApplication={jest.fn()} - application={{ - appName: 'test-app', - instances: [ - { - instanceId: 'instance-1', - clientIp: '123.123.123.123', - lastSeen: '2017-02-23T15:56:49', - sdkVersion: '4.0', - }, - ], - strategies: [ - { - name: 'StrategyA', - description: 'A description', - }, - { - name: 'StrategyB', - description: 'B description', - notFound: true, - }, - ], - seenToggles: [ - { - name: 'ToggleA', - description: 'this is A toggle', - enabled: true, - }, - { - name: 'ToggleB', - description: 'this is B toggle', - enabled: false, - notFound: true, - }, - ], - url: 'http://example.org', - description: 'app description', - }} - location={{ locale: 'en-GB' }} - hasPermission={permission => - [CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION].indexOf(permission) !== -1 - } - /> + + Promise.resolve({})} + storeApplicationMetaData={jest.fn()} + history={{}} + deleteApplication={jest.fn()} + application={{ + appName: 'test-app', + instances: [ + { + instanceId: 'instance-1', + clientIp: '123.123.123.123', + lastSeen: '2017-02-23T15:56:49', + sdkVersion: '4.0', + }, + ], + strategies: [ + { + name: 'StrategyA', + description: 'A description', + }, + { + name: 'StrategyB', + description: 'B description', + notFound: true, + }, + ], + seenToggles: [ + { + name: 'ToggleA', + description: 'this is A toggle', + enabled: true, + }, + { + name: 'ToggleB', + description: 'this is B toggle', + enabled: false, + notFound: true, + }, + ], + url: 'http://example.org', + description: 'app description', + }} + location={{ locale: 'en-GB' }} + hasPermission={permission => + [CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION].indexOf(permission) !== -1 + } + /> + ) .toJSON(); diff --git a/frontend/src/component/application/application-edit-component.js b/frontend/src/component/application/application-edit-component.js index 3da83bde7a..bedd4bc1bf 100644 --- a/frontend/src/component/application/application-edit-component.js +++ b/frontend/src/component/application/application-edit-component.js @@ -2,12 +2,16 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { Button, Card, CardActions, CardTitle, CardText, CardMenu, Icon, ProgressBar, Tabs, Tab } from 'react-mdl'; -import { IconLink, styles as commonStyles } from '../common'; +import { Avatar, Link, Icon, IconButton, Button, LinearProgress, Typography } from '@material-ui/core'; +import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; import { formatFullDateTimeWithLocale, formatDateWithLocale } from '../common/util'; import { UPDATE_APPLICATION } from '../../permissions'; import ApplicationView from './application-view'; import ApplicationUpdate from './application-update'; +import TabNav from '../common/TabNav/TabNav'; +import Dialogue from '../common/Dialogue'; +import PageContent from '../common/PageContent'; +import HeaderTitle from '../common/HeaderTitle'; class ClientApplications extends PureComponent { static propTypes = { @@ -23,7 +27,11 @@ class ClientApplications extends PureComponent { constructor(props) { super(); - this.state = { activeTab: 0, loading: !props.application }; + this.state = { + activeTab: 0, + loading: !props.application, + prompt: false, + }; } componentDidMount() { @@ -34,9 +42,11 @@ class ClientApplications extends PureComponent { deleteApplication = async evt => { evt.preventDefault(); + // if (window.confirm('Are you sure you want to remove this application?')) { const { deleteApplication, appName } = this.props; await deleteApplication(appName); this.props.history.push('/applications'); + // } }; render() { @@ -44,7 +54,7 @@ class ClientApplications extends PureComponent { return (

Loading...

- +
); } else if (!this.props.application) { @@ -53,69 +63,98 @@ class ClientApplications extends PureComponent { const { application, storeApplicationMetaData, hasPermission } = this.props; const { appName, instances, strategies, seenToggles, url, description, icon = 'apps', createdAt } = application; - const content = - this.state.activeTab === 0 ? ( - - ) : ( - - ); + const toggleModal = () => { + this.setState(prev => ({ ...prev, prompt: !prev.prompt })); + }; + + const renderModal = () => ( + + ); + + const tabData = [ + { + label: 'Application overview', + component: ( + + ), + }, + { + label: 'Edit application', + component: ( + + ), + }, + ]; return ( - - - -  {appName} - - -

{description || ''}

-

- Created: {this.formatDate(createdAt)} -

-
- {url && ( - - - - )} - {hasPermission(UPDATE_APPLICATION) ? ( -
- - - - -
- this.setState({ activeTab: tabId })} - ripple - tabBarProps={{ style: { width: '100%' } }} - className="mdl-color--grey-100" - > - Details - Edit - -
- ) : ( - '' - )} + + + {icon || 'apps'} + + {appName} + + } + actions={ + <> + + link + + } + /> - {content} -
+ + Delete + + } + /> + + } + /> + } + > +
+ {description || ''} + + Created: {this.formatDate(createdAt)} + +
+ + {renderModal()} + + + + } + /> +
); } } diff --git a/frontend/src/component/application/application-edit-container.js b/frontend/src/component/application/application-edit-container.js index 7d46fb674b..de708d4ac6 100644 --- a/frontend/src/component/application/application-edit-container.js +++ b/frontend/src/component/application/application-edit-container.js @@ -16,10 +16,10 @@ const mapStateToProps = (state, props) => { }; }; -const Constainer = connect(mapStateToProps, { +const Container = connect(mapStateToProps, { fetchApplication, storeApplicationMetaData, deleteApplication, })(ApplicationEdit); -export default Constainer; +export default Container; diff --git a/frontend/src/component/application/application-list-component.js b/frontend/src/component/application/application-list-component.js index 46416be213..f6076b7f16 100644 --- a/frontend/src/component/application/application-list-component.js +++ b/frontend/src/component/application/application-list-component.js @@ -1,13 +1,15 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { ProgressBar, Card, CardText, Icon } from 'react-mdl'; +import { Icon, CircularProgress } from '@material-ui/core'; import { AppsLinkList, styles as commonStyles } from '../common'; -import SearchField from '../common/search-field'; +import SearchField from '../common/SearchField/SearchField'; +import PageContent from '../common/PageContent/PageContent'; +import HeaderTitle from '../common/HeaderTitle'; const Empty = () => ( - -
+
+ warning

Oh snap, it does not seem like you have connected any applications. To connect your application to Unleash you will require a Client SDK. @@ -15,7 +17,7 @@ const Empty = () => (
You can read more about how to use Unleash in your application in the{' '} documentation. - +
); @@ -35,20 +37,22 @@ class ClientStrategies extends Component { const { applications } = this.props; if (!applications) { - return ; + return ; } return ( -
-
+ <> +
- - {applications.length > 0 ? : } - -
+ }> +
+ {applications.length > 0 ? : } +
+
+ ); } } diff --git a/frontend/src/component/application/application-update.jsx b/frontend/src/component/application/application-update.jsx index 9ab8ef7c69..e4923805b8 100644 --- a/frontend/src/component/application/application-update.jsx +++ b/frontend/src/component/application/application-update.jsx @@ -1,39 +1,51 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Grid, Cell } from 'react-mdl'; -import StatefulTextfield from './stateful-textfield'; +import { TextField, Grid } from '@material-ui/core'; +import { useCommonStyles } from '../../common.styles'; import icons from './icon-names'; import MySelect from '../common/select'; function ApplicationUpdate({ application, storeApplicationMetaData }) { const { appName, icon, url, description } = application; + const [localUrl, setLocalUrl] = useState(url); + const [localDescription, setLocalDescription] = useState(description); + const commonStyles = useCommonStyles(); return ( - - - ({ key: v, label: v }))} - value={icon || 'apps'} - onChange={e => storeApplicationMetaData(appName, 'icon', e.target.value)} - filled - /> - storeApplicationMetaData(appName, 'url', e.target.value)} - /> - -
- storeApplicationMetaData(appName, 'description', e.target.value)} - /> -
+ + + + ({ key: v, label: v }))} + value={icon || 'apps'} + onChange={e => storeApplicationMetaData(appName, 'icon', e.target.value)} + /> + + + setLocalUrl(e.target.value)} + label="Application URL" + placeholder="https://example.com" + type="url" + variant="outlined" + size="small" + onBlur={() => storeApplicationMetaData(appName, 'url', localUrl)} + /> + + + setLocalDescription(e.target.value)} + onBlur={() => storeApplicationMetaData(appName, 'description', localDescription)} + /> + + ); } diff --git a/frontend/src/component/application/application-view.jsx b/frontend/src/component/application/application-view.jsx index e04e8fe18c..5ac900932c 100644 --- a/frontend/src/component/application/application-view.jsx +++ b/frontend/src/component/application/application-view.jsx @@ -1,99 +1,151 @@ import React from 'react'; import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; -import { Grid, Cell, List, ListItem, ListItemContent, Switch } from 'react-mdl'; +import { Grid, List, ListItem, ListItemText, ListItemAvatar, Switch, Icon, Typography } from '@material-ui/core'; import { shorten } from '../common'; import { CREATE_FEATURE, CREATE_STRATEGY } from '../../permissions'; +import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; function ApplicationView({ seenToggles, hasPermission, strategies, instances, formatFullDateTime }) { + const notFoundListItem = ({ createUrl, name, permission }) => ( + + + report + + {name}} + secondary={'Missing, want to create?'} + /> + + } + elseShow={ + + + report + + + + } + /> + ); + + // eslint-disable-next-line react/prop-types + const foundListItem = ({ viewUrl, name, showSwitch, enabled, description, i }) => ( + + + } + elseShow={extension} + /> + + {shorten(name, 50)}} + secondary={shorten(description, 60)} + /> + + ); return ( - - -
Toggles
+ + + + Toggles +
- {seenToggles.map(({ name, description, enabled, notFound }, i) => - notFound ? ( - - {hasPermission(CREATE_FEATURE) ? ( - - {name} - - ) : ( - - {name} - - )} - - ) : ( - - - - - } - subtitle={shorten(description, 60)} - > - {shorten(name, 50)} - - - ) - )} + {seenToggles.map(({ name, description, enabled, notFound }, i) => ( + + ))} -
- -
Implemented strategies
+
+ + + Implemented strategies +
- {strategies.map(({ name, description, notFound }, i) => - notFound ? ( - - {hasPermission(CREATE_STRATEGY) ? ( - - {name} - - ) : ( - - {name} - - )} - - ) : ( - - - {shorten(name, 50)} - - - ) - )} + {strategies.map(({ name, description, notFound }, i) => ( + + ))} - - -
{instances.length} Instances registered
+
+ + + {instances.length} Instances registered +
- {instances.map(({ instanceId, clientIp, lastSeen, sdkVersion }, i) => ( - - ( + + + timeline + + + } + secondary={ {clientIp} last seen at {formatFullDateTime(lastSeen)} } - > - {instanceId} {sdkVersion ? `(${sdkVersion})` : ''} - + /> ))} - +
); } ApplicationView.propTypes = { + createUrl: PropTypes.string, + name: PropTypes.string, + permission: PropTypes.string, instances: PropTypes.array.isRequired, seenToggles: PropTypes.array.isRequired, strategies: PropTypes.array.isRequired, diff --git a/frontend/src/component/application/stateful-textfield.js b/frontend/src/component/application/stateful-textfield.js deleted file mode 100644 index ae4918fcc6..0000000000 --- a/frontend/src/component/application/stateful-textfield.js +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { Textfield } from 'react-mdl'; - -function StatefulTextfield({ value, label, placeholder, rows, onBlur }) { - const [localValue, setLocalValue] = useState(value); - - const onChange = e => { - e.preventDefault(); - setLocalValue(e.target.value); - }; - - return ( - - ); -} - -StatefulTextfield.propTypes = { - value: PropTypes.string, - label: PropTypes.string, - placeholder: PropTypes.string, - rows: PropTypes.number, - onBlur: PropTypes.func.isRequired, -}; - -export default StatefulTextfield; diff --git a/frontend/src/component/archive/archive-list-container.js b/frontend/src/component/archive/archive-list-container.js index c2ce37cda8..12e38e98bc 100644 --- a/frontend/src/component/archive/archive-list-container.js +++ b/frontend/src/component/archive/archive-list-container.js @@ -1,12 +1,12 @@ import { connect } from 'react-redux'; -import FeatureListComponent from './../feature/list/list-component'; +import FeatureListComponent from '../feature/FeatureToggleList/FeatureToggleList'; import { fetchArchive, revive } from './../../store/archive/actions'; import { updateSettingForGroup } from './../../store/settings/actions'; -import { mapStateToPropsConfigurable } from '../feature/list/list-container'; +import { mapStateToPropsConfigurable } from '../feature/FeatureToggleList'; const mapStateToProps = mapStateToPropsConfigurable(false); const mapDispatchToProps = { - fetchArchive, + fetcher: () => fetchArchive(), revive, updateSetting: updateSettingForGroup('feature'), }; diff --git a/frontend/src/component/archive/view-container.js b/frontend/src/component/archive/view-container.js index 234533c225..40d584cd5a 100644 --- a/frontend/src/component/archive/view-container.js +++ b/frontend/src/component/archive/view-container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { fetchArchive, revive } from './../../store/archive/actions'; -import ViewToggleComponent from './../feature/view/view-component'; +import ViewToggleComponent from '../feature/FeatureView/FeatureView'; import { hasPermission } from '../../permissions'; import { fetchTags } from '../../store/feature-tags/actions'; diff --git a/frontend/src/component/common/conditionally-render.jsx b/frontend/src/component/common/ConditionallyRender/ConditionallyRender.jsx similarity index 100% rename from frontend/src/component/common/conditionally-render.jsx rename to frontend/src/component/common/ConditionallyRender/ConditionallyRender.jsx diff --git a/frontend/src/component/common/ConditionallyRender/index.jsx b/frontend/src/component/common/ConditionallyRender/index.jsx new file mode 100644 index 0000000000..474cd34e84 --- /dev/null +++ b/frontend/src/component/common/ConditionallyRender/index.jsx @@ -0,0 +1,3 @@ +import ConditionallyRender from './ConditionallyRender'; + +export default ConditionallyRender; diff --git a/frontend/src/component/common/Dialogue/Dialogue.jsx b/frontend/src/component/common/Dialogue/Dialogue.jsx new file mode 100644 index 0000000000..efd41454d9 --- /dev/null +++ b/frontend/src/component/common/Dialogue/Dialogue.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Dialog, DialogTitle, DialogActions, DialogContent, Button } from '@material-ui/core'; +import PropTypes from 'prop-types'; +import ConditionallyRender from '../ConditionallyRender/ConditionallyRender'; + +const ConfirmDialogue = ({ children, open, onClick, onClose, title, primaryButtonText, secondaryButtonText }) => ( + + {title} + {children}} /> + + + + + + +); + +ConfirmDialogue.propTypes = { + primaryButtonText: PropTypes.string, + secondaryButtonText: PropTypes.string, + children: PropTypes.object, + open: PropTypes.bool, + onClick: PropTypes.func, + onClose: PropTypes.func, + ariaLabel: PropTypes.string, + ariaDescription: PropTypes.string, + title: PropTypes.string, +}; + +export default ConfirmDialogue; diff --git a/frontend/src/component/common/Dialogue/index.jsx b/frontend/src/component/common/Dialogue/index.jsx new file mode 100644 index 0000000000..815bc7f9c2 --- /dev/null +++ b/frontend/src/component/common/Dialogue/index.jsx @@ -0,0 +1,3 @@ +import Dialogue from './Dialogue'; + +export default Dialogue; diff --git a/frontend/src/component/common/HeaderTitle/HeaderTitle.jsx b/frontend/src/component/common/HeaderTitle/HeaderTitle.jsx new file mode 100644 index 0000000000..9923849daa --- /dev/null +++ b/frontend/src/component/common/HeaderTitle/HeaderTitle.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; + +import { Typography } from '@material-ui/core'; +import ConditionallyRender from '../ConditionallyRender/ConditionallyRender'; + +import { useStyles } from './styles'; + +const HeaderTitle = ({ title, actions, subtitle, variant, loading }) => { + const styles = useStyles(); + const headerClasses = classnames({ skeleton: loading }); + + return ( +
+
+ + {title} + + {subtitle && {subtitle}} +
+ + {actions}
} /> +
+ ); +}; + +export default HeaderTitle; + +HeaderTitle.propTypes = { + title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), + subtitle: PropTypes.string, + variant: PropTypes.string, + loading: PropTypes.bool, + actions: PropTypes.element, +}; diff --git a/frontend/src/component/common/HeaderTitle/index.jsx b/frontend/src/component/common/HeaderTitle/index.jsx new file mode 100644 index 0000000000..a2a7ea9d8d --- /dev/null +++ b/frontend/src/component/common/HeaderTitle/index.jsx @@ -0,0 +1,3 @@ +import HeaderTitle from './HeaderTitle'; + +export default HeaderTitle; diff --git a/frontend/src/component/common/HeaderTitle/styles.js b/frontend/src/component/common/HeaderTitle/styles.js new file mode 100644 index 0000000000..44f6a9189c --- /dev/null +++ b/frontend/src/component/common/HeaderTitle/styles.js @@ -0,0 +1,18 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + headerTitleContainer: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + position: 'relative', + }, + headerTitle: { + fontSize: theme.fontSizes.mainHeader, + fontWeight: 500, + }, + headerActions: { + position: 'absolute', + right: 0, + }, +})); diff --git a/frontend/src/component/common/PageContent/PageContent.jsx b/frontend/src/component/common/PageContent/PageContent.jsx new file mode 100644 index 0000000000..ba71c3c0cf --- /dev/null +++ b/frontend/src/component/common/PageContent/PageContent.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import classnames from 'classnames'; +import HeaderTitle from '../HeaderTitle'; +import { Paper } from '@material-ui/core'; +import { useStyles } from './styles'; + +const PageContent = ({ children, headerContent, disablePadding, disableBorder, ...rest }) => { + const styles = useStyles(); + + const headerClasses = classnames(styles.headerContainer, { + [styles.paddingDisabled]: disablePadding, + [styles.borderDisabled]: disableBorder, + }); + + const bodyClasses = classnames(styles.bodyContainer, { + [styles.paddingDisabled]: disablePadding, + [styles.borderDisabled]: disableBorder, + }); + + let header = null; + if (headerContent) { + if (typeof headerContent === 'string') { + header = ( +
+ +
+ ); + } else { + header =
{headerContent}
; + } + } + + const paperProps = disableBorder ? { elevation: 0 } : {}; + + return ( + + {header} +
{children}
+
+ ); +}; + +export default PageContent; + +PageContent.propTypes = { + headerContent: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), + disablePadding: PropTypes.bool, + disableBorder: PropTypes.bool, +}; diff --git a/frontend/src/component/common/PageContent/index.jsx b/frontend/src/component/common/PageContent/index.jsx new file mode 100644 index 0000000000..29126440c9 --- /dev/null +++ b/frontend/src/component/common/PageContent/index.jsx @@ -0,0 +1,3 @@ +import PageContent from './PageContent'; + +export default PageContent; diff --git a/frontend/src/component/common/PageContent/styles.js b/frontend/src/component/common/PageContent/styles.js new file mode 100644 index 0000000000..266ef3efd4 --- /dev/null +++ b/frontend/src/component/common/PageContent/styles.js @@ -0,0 +1,23 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + headerContainer: { + padding: theme.padding.pageContent.header, + borderBottom: theme.borders.default, + [theme.breakpoints.down('sm')]: { + padding: '1.5rem 1rem', + }, + }, + bodyContainer: { + padding: theme.padding.pageContent.body, + [theme.breakpoints.down('sm')]: { + padding: '1rem', + }, + }, + paddingDisabled: { + padding: '0', + }, + borderDisabled: { + border: 'none', + }, +})); diff --git a/frontend/src/component/common/ProjectSelect/ProjectSelect.jsx b/frontend/src/component/common/ProjectSelect/ProjectSelect.jsx new file mode 100644 index 0000000000..58175aeda2 --- /dev/null +++ b/frontend/src/component/common/ProjectSelect/ProjectSelect.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { MenuItem } from '@material-ui/core'; +import PropTypes from 'prop-types'; +import DropdownMenu from '../dropdown-menu'; + +const ALL_PROJECTS = { id: '*', name: '> All projects' }; + +const ProjectSelect = ({ projects, currentProjectId, updateCurrentProject }) => { + const setProject = v => { + const id = typeof v === 'string' ? v.trim() : ''; + updateCurrentProject(id); + }; + + if (!projects || projects.length === 1) { + return null; + } + + // TODO fixme + let curentProject = projects.find(i => i.id === currentProjectId); + if (!curentProject) { + curentProject = ALL_PROJECTS; + } + + const handleChangeProject = e => { + const target = e.target.getAttribute('data-target'); + setProject(target); + }; + + const renderProjectItem = (selectedId, item) => ( + + {item.name} + + ); + + const renderProjectOptions = () => { + const start = [ + + {ALL_PROJECTS.name} + , + ]; + + return [...start, ...projects.map(p => renderProjectItem(currentProjectId, p))]; + }; + + return ( + + + + ); +}; + +ProjectSelect.propTypes = { + projects: PropTypes.array.isRequired, + fetchProjects: PropTypes.func.isRequired, + currentProjectId: PropTypes.string.isRequired, + updateCurrentProject: PropTypes.func.isRequired, +}; + +export default ProjectSelect; diff --git a/frontend/src/component/feature/list/project-container.jsx b/frontend/src/component/common/ProjectSelect/index.jsx similarity index 50% rename from frontend/src/component/feature/list/project-container.jsx rename to frontend/src/component/common/ProjectSelect/index.jsx index 80eca17d19..a7fa1bda65 100644 --- a/frontend/src/component/feature/list/project-container.jsx +++ b/frontend/src/component/common/ProjectSelect/index.jsx @@ -1,13 +1,11 @@ import { connect } from 'react-redux'; -import Component from './project-component'; -import { fetchProjects } from './../../../store/project/actions'; -import { P } from '../../common/flags'; +import ProjectSelect from './ProjectSelect'; +import { fetchProjects } from '../../../store/project/actions'; const mapStateToProps = (state, ownProps) => ({ - enabled: !!state.uiConfig.toJS().flags[P], projects: state.projects.toJS(), currentProjectId: ownProps.settings.currentProjectId || '*', updateCurrentProject: id => ownProps.updateSetting('currentProjectId', id), }); -export default connect(mapStateToProps, { fetchProjects })(Component); +export default connect(mapStateToProps, { fetchProjects })(ProjectSelect); diff --git a/frontend/src/component/common/SearchField/SearchField.jsx b/frontend/src/component/common/SearchField/SearchField.jsx new file mode 100644 index 0000000000..8807186039 --- /dev/null +++ b/frontend/src/component/common/SearchField/SearchField.jsx @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import { debounce } from 'debounce'; +import { InputBase } from '@material-ui/core'; +import SearchIcon from '@material-ui/icons/Search'; + +import { useStyles } from './styles'; + +function SearchField({ value = '', updateValue, className }) { + const styles = useStyles(); + + const [localValue, setLocalValue] = useState(value); + const debounceUpdateValue = debounce(updateValue, 500); + + const handleCange = e => { + e.preventDefault(); + const v = e.target.value || ''; + setLocalValue(v); + debounceUpdateValue(v); + }; + + const handleKeyPress = e => { + if (e.key === 'Enter') { + updateValue(localValue); + } + }; + + const updateNow = () => { + updateValue(localValue); + }; + + return ( +
+
+ + +
+
+ ); +} + +SearchField.propTypes = { + value: PropTypes.string, + updateValue: PropTypes.func.isRequired, +}; + +export default SearchField; diff --git a/frontend/src/component/common/SearchField/index.jsx b/frontend/src/component/common/SearchField/index.jsx new file mode 100644 index 0000000000..8a4300dca9 --- /dev/null +++ b/frontend/src/component/common/SearchField/index.jsx @@ -0,0 +1,3 @@ +import SearchField from './SearchField'; + +export default SearchField; diff --git a/frontend/src/component/common/SearchField/styles.js b/frontend/src/component/common/SearchField/styles.js new file mode 100644 index 0000000000..2b05321c1c --- /dev/null +++ b/frontend/src/component/common/SearchField/styles.js @@ -0,0 +1,24 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + search: { + display: 'flex', + alignItems: 'center', + backgroundColor: theme.palette.searchField.main, + borderRadius: theme.borders.radius.main, + padding: '0.25rem 0.5rem', + maxWidth: '450px', + [theme.breakpoints.down('sm')]: { + margin: '0 auto', + }, + [theme.breakpoints.down('xs')]: { + width: '100%', + }, + }, + searchIcon: { + marginRight: '8px', + }, + inputRoot: { + width: '100%', + }, +})); diff --git a/frontend/src/component/common/TabNav/TabNav.jsx b/frontend/src/component/common/TabNav/TabNav.jsx new file mode 100644 index 0000000000..a068a575a3 --- /dev/null +++ b/frontend/src/component/common/TabNav/TabNav.jsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Tabs, Tab, Paper } from '@material-ui/core'; + +import TabPanel from './TabPanel'; + +import { useStyles } from './styles'; +import { useHistory } from 'react-router-dom'; + +const a11yProps = index => ({ + id: `tab-${index}`, + 'aria-controls': `tabpanel-${index}`, +}); + +const TabNav = ({ tabData, className, startingTab = 0 }) => { + const styles = useStyles(); + const [activeTab, setActiveTab] = useState(startingTab); + const history = useHistory(); + + const renderTabs = () => + tabData.map((tab, index) => ( + history.push(tab.path)} + /> + )); + + const renderTabPanels = () => + tabData.map((tab, index) => ( + + {tab.component} + + )); + + return ( + <> + + { + setActiveTab(tabId); + }} + indicatorColor="primary" + textColor="primary" + centered + > + {renderTabs()} + + +
{renderTabPanels()}
+ + ); +}; + +TabNav.propTypes = { + tabData: PropTypes.array.isRequired, + className: PropTypes.string, + startingTab: PropTypes.number, +}; + +export default TabNav; diff --git a/frontend/src/component/common/TabNav/TabPanel/TabPanel.jsx b/frontend/src/component/common/TabNav/TabPanel/TabPanel.jsx new file mode 100644 index 0000000000..efad562c75 --- /dev/null +++ b/frontend/src/component/common/TabNav/TabPanel/TabPanel.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const TabPanel = ({ children, value, index, ...other }) => ( + +); + +TabPanel.propTypes = { + value: PropTypes.number, + index: PropTypes.number, + children: PropTypes.object, +}; + +export default TabPanel; diff --git a/frontend/src/component/common/TabNav/TabPanel/index.jsx b/frontend/src/component/common/TabNav/TabPanel/index.jsx new file mode 100644 index 0000000000..a09b57d836 --- /dev/null +++ b/frontend/src/component/common/TabNav/TabPanel/index.jsx @@ -0,0 +1,3 @@ +import TabPanel from './TabPanel'; + +export default TabPanel; diff --git a/frontend/src/component/common/TabNav/index.jsx b/frontend/src/component/common/TabNav/index.jsx new file mode 100644 index 0000000000..2d11e237e2 --- /dev/null +++ b/frontend/src/component/common/TabNav/index.jsx @@ -0,0 +1,3 @@ +import TabNav from './TabNav'; + +export default TabNav; diff --git a/frontend/src/component/common/TabNav/styles.js b/frontend/src/component/common/TabNav/styles.js new file mode 100644 index 0000000000..eb50bcc0fc --- /dev/null +++ b/frontend/src/component/common/TabNav/styles.js @@ -0,0 +1,7 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + tabNav: { + backgroundColor: theme.palette.tabs.main, + }, +})); diff --git a/frontend/src/component/common/common.module.scss b/frontend/src/component/common/common.module.scss index 62d9f0b801..b35152d53a 100644 --- a/frontend/src/component/common/common.module.scss +++ b/frontend/src/component/common/common.module.scss @@ -1,3 +1,5 @@ +/** Select **/ + .truncate { white-space: nowrap; overflow: hidden; @@ -42,27 +44,21 @@ display: flex; justify-content: space-between; align-items: center; - height: 64px; - - .title { - padding: 20px 16px 20px 24px; - } .titleText { margin: 0; font-size: 20px; - line-height: 24px + line-height: 24px; } .actions { flex-shrink: 0; - padding: 20px 14px 20px 16px; } } .switchWithLabel { display: flex; - + .label { padding-right: 16px; line-height: 24px; @@ -106,4 +102,30 @@ .error { color: #d50000; -} \ No newline at end of file +} + +.headerTitle { + font-size: var(--h1-size); + margin: 0; +} + +.listItem { + padding: 0; +} + +.section { + padding: 8px 16px 8px 16px; +} + +.contentPadding { + padding: var(--card-padding); +} + +.contentSpacing > * { + margin: 0.5rem 0; +} + +.searchField { + margin-bottom: 2rem; + max-width: 400px; +} diff --git a/frontend/src/component/common/dropdown-menu.jsx b/frontend/src/component/common/dropdown-menu.jsx new file mode 100644 index 0000000000..40561229ef --- /dev/null +++ b/frontend/src/component/common/dropdown-menu.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Menu } from '@material-ui/core'; +import { DropdownButton } from '.'; + +import styles from './common.module.scss'; + +const DropdownMenu = ({ renderOptions, id, title, callback, icon = 'arrow_drop_down', label, startIcon, ...rest }) => { + const [anchor, setAnchor] = React.useState(null); + + const handleOpen = e => setAnchor(e.currentTarget); + + const handleClose = e => { + if (callback && typeof callback === 'function') { + callback(e); + } + + setAnchor(null); + }; + + return ( + <> + + + {renderOptions()} + + + ); +}; + +DropdownMenu.propTypes = { + renderOptions: PropTypes.func, + id: PropTypes.string, + title: PropTypes.string, + callback: PropTypes.func, + icon: PropTypes.string, + label: PropTypes.string, + startIcon: PropTypes.string, +}; + +export default DropdownMenu; diff --git a/frontend/src/component/common/index.js b/frontend/src/component/common/index.js index 5e207a462a..6ccda01301 100644 --- a/frontend/src/component/common/index.js +++ b/frontend/src/component/common/index.js @@ -1,62 +1,65 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; -import { List, ListItem, ListItemContent, Button, Icon, Switch, MenuItem } from 'react-mdl'; +import { + List, + MenuItem, + Icon, + ListItem, + ListItemText, + ListItemAvatar, + Button, + Avatar, + Typography, +} from '@material-ui/core'; import styles from './common.module.scss'; +import ConditionallyRender from './ConditionallyRender/ConditionallyRender'; export { styles }; export const shorten = (str, len = 50) => (str && str.length > len ? `${str.substring(0, len)}...` : str); - export const AppsLinkList = ({ apps }) => ( - {apps.length > 0 && - apps.map(({ appName, description, icon }) => ( - - - - - {appName} - - {description || 'No description'} - - - + 0} + show={apps.map(({ appName, description, icon }) => ( + + + + {icon}} + elseShow={apps} + /> + + + + {appName} + + } + secondary={description || 'No description'} + /> ))} + /> ); AppsLinkList.propTypes = { apps: PropTypes.array.isRequired, }; -export const HeaderTitle = ({ title, actions, subtitle }) => ( -
-
-
{title}
- {subtitle && {subtitle}} -
- - {actions &&
{actions}
} -
-); -HeaderTitle.propTypes = { - title: PropTypes.string, - subtitle: PropTypes.string, - actions: PropTypes.any, -}; - export const DataTableHeader = ({ title, actions }) => (
-

{title}

+ + {title} +
{actions &&
{actions}
}
@@ -66,11 +69,15 @@ DataTableHeader.propTypes = { actions: PropTypes.any, }; -export const FormButtons = ({ submitText = 'Create', onCancel }) => ( +export const FormButtons = ({ submitText = 'Create', onCancel, primaryButtonTestId }) => (
-   @@ -82,37 +89,7 @@ export const FormButtons = ({ submitText = 'Create', onCancel }) => ( FormButtons.propTypes = { submitText: PropTypes.string, onCancel: PropTypes.func.isRequired, -}; - -export const SwitchWithLabel = ({ onChange, checked, children, ...switchProps }) => ( - - {children} - - - - -); -SwitchWithLabel.propTypes = { - checked: PropTypes.bool, - onChange: PropTypes.func, -}; - -export const TogglesLinkList = ({ toggles }) => ( - - {toggles.length > 0 && - toggles.map(({ name, description = '-', icon = 'toggle' }) => ( - - - - {name} - - - - ))} - -); -TogglesLinkList.propTypes = { - toggles: PropTypes.array, + primaryButtonTestId: PropTypes.string, }; export function getIcon(type) { @@ -132,7 +109,7 @@ export function getIcon(type) { export const IconLink = ({ url, icon }) => ( - + {icon} ); IconLink.propTypes = { @@ -140,22 +117,41 @@ IconLink.propTypes = { icon: PropTypes.string, }; -export const DropdownButton = ({ label, id, className = styles.dropdownButton, title, style }) => ( - ); + DropdownButton.propTypes = { label: PropTypes.string, style: PropTypes.object, id: PropTypes.string, title: PropTypes.string, + icon: PropTypes.string, + startIcon: PropTypes.string, }; export const MenuItemWithIcon = ({ icon, label, disabled, ...menuItemProps }) => ( - + {icon} {label} ); diff --git a/frontend/src/component/common/input-list-field.jsx b/frontend/src/component/common/input-list-field.jsx index a426ac6c9d..cf4b9c8faa 100644 --- a/frontend/src/component/common/input-list-field.jsx +++ b/frontend/src/component/common/input-list-field.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Textfield } from 'react-mdl'; +import { TextField } from '@material-ui/core'; function InputListField({ label, values = [], error, name, updateValues, placeholder = '', onBlur = () => {} }) { const handleChange = evt => { @@ -21,10 +21,10 @@ function InputListField({ label, values = [], error, name, updateValues, placeho }; return ( - ); } diff --git a/frontend/src/component/common/search-field.jsx b/frontend/src/component/common/search-field.jsx deleted file mode 100644 index 7ab69a5af8..0000000000 --- a/frontend/src/component/common/search-field.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { debounce } from 'debounce'; -import { FABButton, Icon, Textfield } from 'react-mdl'; - -function SearchField({ value = '', updateValue }) { - const [localValue, setLocalValue] = useState(value); - const debounceUpdateValue = debounce(updateValue, 500); - - const handleCange = e => { - e.preventDefault(); - const v = e.target.value || ''; - setLocalValue(v); - debounceUpdateValue(v); - }; - - const handleKeyPress = e => { - if (e.key === 'Enter') { - updateValue(localValue); - } - }; - - const updateNow = () => { - updateValue(localValue); - }; - - return ( -
- - - - -
- ); -} - -SearchField.propTypes = { - value: PropTypes.string, - updateValue: PropTypes.func.isRequired, -}; - -export default SearchField; diff --git a/frontend/src/component/common/select.jsx b/frontend/src/component/common/select.jsx index de30592b1e..4fa92597c3 100644 --- a/frontend/src/component/common/select.jsx +++ b/frontend/src/component/common/select.jsx @@ -1,50 +1,46 @@ import React from 'react'; -import classnames from 'classnames'; +import { Select, FormControl, MenuItem, InputLabel } from '@material-ui/core'; import PropTypes from 'prop-types'; -const Select = ({ name, value, label, options, style, onChange, disabled = false, filled, className }) => { - const wrapper = Object.assign({ width: 'auto' }, style); +const SelectMenu = ({ name, value, label, options, onChange, id, disabled = false, className, ...rest }) => { + const renderSelectItems = () => + options.map(option => ( + + {option.label} + + )); + return ( -
- - {options.map(o => ( - - ))} - - -
+ {renderSelectItems()} + + ); }; -Select.propTypes = { +SelectMenu.propTypes = { name: PropTypes.string, + id: PropTypes.string, value: PropTypes.string, label: PropTypes.string, options: PropTypes.array, style: PropTypes.object, onChange: PropTypes.func.isRequired, disabled: PropTypes.bool, - filled: PropTypes.bool, }; -export default Select; +export default SelectMenu; diff --git a/frontend/src/component/context/Context.module.scss b/frontend/src/component/context/Context.module.scss new file mode 100644 index 0000000000..b320f35907 --- /dev/null +++ b/frontend/src/component/context/Context.module.scss @@ -0,0 +1,65 @@ +.header { + padding: var(--card-header-padding); + margin-bottom: var(--card-margin-y); + word-break: break-all; + border-bottom: var(--default-border); + display: flex; + align-items: center; + justify-content: space-between; +} + +.header h1 { + font-size: var(--h1-size); +} + +.formButtons { + padding-top: 1rem; +} + +.supporting { + font-size: var(--caption-size); + max-width: 450px; +} + +.container { + padding: var(--card-padding); +} + +.container section { + margin: 1rem 0 +} + +.h6 { + margin-top: 0; +} + +.alpha { + color: rgba(0,0,0,.54); +} + +.inset { + background-color: rgb(250, 250, 250); + padding: var(--card-padding); + max-width: 450px; +} + +.chip { + margin-right: 4px; +} + +.valueField { + width: 130px; +} + +.legalValueButton { + margin-left: 10px; +} + +.formContainer { + margin-bottom: 1.5rem; + max-width: 350px; +} + +.formContainer > *, .inset > * { + margin: 0.5rem 0; +} diff --git a/frontend/src/component/context/ContextList/ContextList.jsx b/frontend/src/component/context/ContextList/ContextList.jsx new file mode 100644 index 0000000000..7c5279ab25 --- /dev/null +++ b/frontend/src/component/context/ContextList/ContextList.jsx @@ -0,0 +1,94 @@ +import PropTypes from 'prop-types'; +import PageContent from '../../common/PageContent/PageContent'; +import HeaderTitle from '../../common/HeaderTitle'; +import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; +import { CREATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } from '../../../permissions'; +import { Icon, IconButton, List, ListItem, ListItemIcon, ListItemText, Tooltip } from '@material-ui/core'; +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { useStyles } from './styles'; +import ConfirmDialogue from '../../common/Dialogue'; + +const ContextList = ({ removeContextField, hasPermission, history, contextFields }) => { + const [showDelDialogue, setShowDelDialogue] = useState(false); + const [name, setName] = useState(); + + const styles = useStyles(); + const contextList = () => + contextFields.map(field => ( + + + album + + + {field.name} + + } + secondary={field.description} + /> + + { + setName(field.name); + setShowDelDialogue(true); + }} + > + delete + + + } + /> + + )); + const headerButton = () => ( + + history.push('/context/create')}> + add + + + } + /> + ); + return ( + }> + + 0} + show={contextList} + elseShow={No context fields defined} + /> + + { + removeContextField({ name }); + setName(undefined); + setShowDelDialogue(false); + }} + onClose={() => { + setName(undefined); + setShowDelDialogue(false); + }} + title="Really delete context field" + /> + + ); +}; + +ContextList.propTypes = { + contextFields: PropTypes.array.isRequired, + removeContextField: PropTypes.func.isRequired, + history: PropTypes.object.isRequired, + hasPermission: PropTypes.func.isRequired, +}; + +export default ContextList; diff --git a/frontend/src/component/context/list-container.jsx b/frontend/src/component/context/ContextList/index.jsx similarity index 53% rename from frontend/src/component/context/list-container.jsx rename to frontend/src/component/context/ContextList/index.jsx index c6de606161..56fb6afda7 100644 --- a/frontend/src/component/context/list-container.jsx +++ b/frontend/src/component/context/ContextList/index.jsx @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; -import ContextFieldListComponent from './list-component.jsx'; -import { fetchContext, removeContextField } from './../../store/context/actions'; -import { hasPermission } from '../../permissions'; +import ContextList from './ContextList'; +import { fetchContext, removeContextField } from '../../../store/context/actions'; +import { hasPermission } from '../../../permissions'; const mapStateToProps = state => { const list = state.context.toJS(); @@ -14,14 +14,11 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => ({ removeContextField: contextField => { - // eslint-disable-next-line no-alert - if (window.confirm('Are you sure you want to remove this context field?')) { - removeContextField(contextField)(dispatch); - } + removeContextField(contextField)(dispatch); }, fetchContext: () => fetchContext()(dispatch), }); -const ContextFieldListContainer = connect(mapStateToProps, mapDispatchToProps)(ContextFieldListComponent); +const ContextFieldListContainer = connect(mapStateToProps, mapDispatchToProps)(ContextList); export default ContextFieldListContainer; diff --git a/frontend/src/component/context/ContextList/styles.js b/frontend/src/component/context/ContextList/styles.js new file mode 100644 index 0000000000..1fd39fd466 --- /dev/null +++ b/frontend/src/component/context/ContextList/styles.js @@ -0,0 +1,7 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles({ + listItem: { + padding: 0, + }, +}); diff --git a/frontend/src/component/context/form-context-component.jsx b/frontend/src/component/context/form-context-component.jsx index 8da86e2214..0b6d320049 100644 --- a/frontend/src/component/context/form-context-component.jsx +++ b/frontend/src/component/context/form-context-component.jsx @@ -1,9 +1,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { Button, Chip, Textfield, Card, CardTitle, CardText, CardActions, Checkbox } from 'react-mdl'; - +import { Button, Chip, TextField, Switch, Icon, Typography } from '@material-ui/core'; +import styles from './Context.module.scss'; +import classnames from 'classnames'; import { FormButtons, styles as commonStyles } from '../common'; import { trim } from '../common/util'; +import PageContent from '../common/PageContent/PageContent'; const sortIgnoreCase = (a, b) => { a = a.toLowerCase(); @@ -99,12 +101,10 @@ class AddContextComponent extends Component { renderLegalValue = (value, index) => ( this.removeLegalValue(index)} - > - {value} - + className={styles.chip} + onDelete={() => this.removeLegalValue(index)} + label={value} + /> ); render() { @@ -113,85 +113,96 @@ class AddContextComponent extends Component { const submitText = editMode ? 'Update' : 'Create'; return ( - - - Create context field - - + +
Context fields are a basic building block used in Unleash to control roll-out. They can be used together with strategy constraints as part of the activation strategy evaluation. - +
-
- + this.validateContextName(v.target.value)} onChange={v => this.setValue('name', trim(v.target.value))} /> - this.setValue('description', v.target.value)} />

-
-
Legal values
-

- By defining the legal values the Unleash Admin UI will validate the user input. A - concrete example would be that we know all values for our “environment” (local, - development, stage, production). -

- +
+
Legal values
+

+ By defining the legal values the Unleash Admin UI will validate the user input. A concrete + example would be that we know all values for our “environment” (local, development, stage, + production). +

+
+ - -
{contextField.legalValues.map(this.renderLegalValue)}
-
-
-
-
Custom stickiness (beta)
-

- By enabling stickiness on this context field you can use it together with the - flexible-rollout strategy. This will guarantee a consistent behavior for specific values - of this context field. PS! Not all client SDK's support this feature yet!{' '} - - Read more - -

- this.setValue('stickiness', !contextField.stickiness)} - /> -
+
+
{contextField.legalValues.map(this.renderLegalValue)}
- +
+
+ Custom stickiness (beta) +

+ By enabling stickiness on this context field you can use it together with the + flexible-rollout strategy. This will guarantee a consistent behavior for specific values of + this context field. PS! Not all client SDK's support this feature yet!{' '} + + Read more + +

+ this.setValue('stickiness', !contextField.stickiness)} + /> +
+
- +
-
+ ); } } diff --git a/frontend/src/component/context/list-component.jsx b/frontend/src/component/context/list-component.jsx deleted file mode 100644 index da79527a59..0000000000 --- a/frontend/src/component/context/list-component.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; - -import { List, ListItem, ListItemAction, ListItemContent, IconButton, Card } from 'react-mdl'; -import { HeaderTitle, styles as commonStyles } from '../common'; -import { CREATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } from '../../permissions'; - -class ContextFieldListComponent extends Component { - static propTypes = { - contextFields: PropTypes.array.isRequired, - fetchContext: PropTypes.func.isRequired, - removeContextField: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, - hasPermission: PropTypes.func.isRequired, - }; - - componentDidMount() { - // this.props.fetchContext(); - } - - removeContextField = (contextField, evt) => { - evt.preventDefault(); - this.props.removeContextField(contextField); - }; - - render() { - const { contextFields, hasPermission } = this.props; - - return ( - - this.props.history.push('/context/create')} - title="Add new context field" - /> - ) : ( - '' - ) - } - /> - - {contextFields.length > 0 ? ( - contextFields.map((field, i) => ( - - - - {field.name} - - - - {hasPermission(DELETE_CONTEXT_FIELD) ? ( - - ) : ( - '' - )} - - - )) - ) : ( - No context fields defined - )} - - - ); - } -} - -export default ContextFieldListComponent; diff --git a/frontend/src/component/error/error-component.jsx b/frontend/src/component/error/error-component.jsx index 898b24a1d2..01e2b182da 100644 --- a/frontend/src/component/error/error-component.jsx +++ b/frontend/src/component/error/error-component.jsx @@ -1,16 +1,31 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Snackbar, Icon } from 'react-mdl'; +import { Snackbar, Icon, IconButton } from '@material-ui/core'; const ErrorComponent = ({ errors, ...props }) => { const showError = errors.length > 0; const error = showError ? errors[0] : undefined; const muteError = () => props.muteError(error); return ( - - {error} - + + + close + + + } + open={showError} + onClose={muteError} + autoHideDuration={10000} + message={ +
+ question_answer + {error} +
+ } + /> ); }; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleList.jsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleList.jsx new file mode 100644 index 0000000000..ed4b74c709 --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleList.jsx @@ -0,0 +1,172 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { Link } from 'react-router-dom'; +import { Button, List, Tooltip, IconButton, Icon } from '@material-ui/core'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; + +import FeatureToggleListItem from './FeatureToggleListItem'; +import SearchField from '../../common/SearchField/SearchField'; +import FeatureToggleListActions from './FeatureToggleListActions'; +import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; +import PageContent from '../../common/PageContent/PageContent'; +import HeaderTitle from '../../common/HeaderTitle'; + +import loadingFeatures from './loadingFeatures'; + +import { CREATE_FEATURE } from '../../../permissions'; + +import { useStyles } from './styles'; + +const FeatureToggleList = ({ + fetcher, + features, + hasPermission, + settings, + revive, + updateSetting, + featureMetrics, + toggleFeature, + loading, +}) => { + const styles = useStyles(); + const smallScreen = useMediaQuery('(max-width:700px)'); + + useEffect(() => { + fetcher(); + }, [fetcher]); + + const toggleMetrics = () => { + updateSetting('showLastHour', !settings.showLastHour); + }; + + const setSort = v => { + updateSetting('sort', typeof v === 'string' ? v.trim() : ''); + }; + + const renderFeatures = () => { + features.forEach(e => { + e.reviveName = e.name; + }); + + if (loading) { + return loadingFeatures.map(feature => ( + + )); + } + + return features.map(feature => ( + + )); + }; + + return ( +
+
+ +
+ + + + } + /> + + + + add + + + } + elseShow={ + + } + /> + } + /> +
+ } + /> + } + > + {renderFeatures()} + + + ); +}; + +FeatureToggleList.propTypes = { + features: PropTypes.array.isRequired, + featureMetrics: PropTypes.object.isRequired, + fetcher: PropTypes.func, + revive: PropTypes.func, + updateSetting: PropTypes.func.isRequired, + toggleFeature: PropTypes.func, + settings: PropTypes.object, + history: PropTypes.object.isRequired, + hasPermission: PropTypes.func.isRequired, + loading: PropTypes.bool, +}; + +export default FeatureToggleList; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListActions/FeatureToggleListActions.jsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListActions/FeatureToggleListActions.jsx new file mode 100644 index 0000000000..f83d377e82 --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListActions/FeatureToggleListActions.jsx @@ -0,0 +1,88 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { MenuItem } from '@material-ui/core'; +import { MenuItemWithIcon } from '../../../common'; +import DropdownMenu from '../../../common/dropdown-menu'; +import ProjectSelect from '../../../common/ProjectSelect'; + +const sortingOptions = [ + { type: 'name', displayName: 'Name' }, + { type: 'type', displayName: 'Type' }, + { type: 'enabled', displayName: 'Enabled' }, + { type: 'stale', displayName: 'Stale' }, + { type: 'created', displayName: 'Created' }, + { type: 'Last seen', displayName: 'Last seen' }, + { type: 'strategies', displayName: 'Strategies' }, + { type: 'metrics', displayName: 'Metrics' }, +]; + +import { useStyles } from './styles'; +import classnames from 'classnames'; + +const FeatureToggleListActions = ({ settings, setSort, toggleMetrics, updateSetting, loading }) => { + const styles = useStyles(); + + const handleSort = e => { + const target = e.target.getAttribute('data-target'); + setSort(target); + }; + + const isDisabled = type => settings.sort === type; + + const renderSortingOptions = () => + sortingOptions.map(option => ( + + {option.displayName} + + )); + + const renderMetricsOptions = () => [ + , + , + ]; + + return ( +
+ + + +
+ ); +}; + +FeatureToggleListActions.propTypes = { + settings: PropTypes.object, + setSort: PropTypes.func, + toggleMetrics: PropTypes.func, + updateSetting: PropTypes.func, + loading: PropTypes.bool, +}; + +export default FeatureToggleListActions; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListActions/index.jsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListActions/index.jsx new file mode 100644 index 0000000000..18b19140dd --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListActions/index.jsx @@ -0,0 +1,3 @@ +import FeatureToggleListActions from './FeatureToggleListActions'; + +export default FeatureToggleListActions; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListActions/styles.js b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListActions/styles.js new file mode 100644 index 0000000000..a9c119e368 --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListActions/styles.js @@ -0,0 +1,10 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles({ + actions: { + '& > *': { + margin: '0 0.25rem', + }, + marginRight: '0.25rem', + }, +}); diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItem.jsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItem.jsx new file mode 100644 index 0000000000..03248770dc --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItem.jsx @@ -0,0 +1,98 @@ +import React, { memo } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +import { Link } from 'react-router-dom'; +import { Switch, Icon, IconButton, ListItem } from '@material-ui/core'; +import TimeAgo from 'react-timeago'; +import Progress from '../../progress-component'; +import Status from '../../status-component'; +import FeatureToggleListItemChip from './FeatureToggleListItemChip'; +import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender'; + +import { UPDATE_FEATURE } from '../../../../permissions'; +import { calc, styles as commonStyles } from '../../../common'; + +import { useStyles } from './styles'; + +const FeatureToggleListItem = ({ + feature, + toggleFeature, + settings, + metricsLastHour = { yes: 0, no: 0, isFallback: true }, + metricsLastMinute = { yes: 0, no: 0, isFallback: true }, + revive, + hasPermission, + ...rest +}) => { + const styles = useStyles(); + + const { name, description, enabled, type, stale, createdAt } = feature; + const { showLastHour = false } = settings; + const isStale = showLastHour ? metricsLastHour.isFallback : metricsLastMinute.isFallback; + const percent = + 1 * + (showLastHour + ? calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0) + : calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0)); + const featureUrl = toggleFeature === undefined ? `/archive/strategies/${name}` : `/features/strategies/${name}`; + + return ( + + + + + + toggleFeature(!enabled, name)} + checked={enabled} + /> + } + elseShow={} + /> + + + + {name}  + + + +
+ {description} +
+ +
+ + + + + revive(feature.name)}> + undo + + } + elseShow={} + /> +
+ ); +}; + +FeatureToggleListItem.propTypes = { + feature: PropTypes.object, + toggleFeature: PropTypes.func, + settings: PropTypes.object, + metricsLastHour: PropTypes.object, + metricsLastMinute: PropTypes.object, + revive: PropTypes.func, + hasPermission: PropTypes.func.isRequired, +}; + +export default memo(FeatureToggleListItem); diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItemChip/FeatureToggleListItemChip.jsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItemChip/FeatureToggleListItemChip.jsx new file mode 100644 index 0000000000..819f62627c --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItemChip/FeatureToggleListItemChip.jsx @@ -0,0 +1,26 @@ +import React, { memo } from 'react'; +import { Chip } from '@material-ui/core'; +import PropTypes from 'prop-types'; + +import { useStyles } from './styles'; + +const FeatureToggleListItemChip = ({ type, types, onClick }) => { + const styles = useStyles(); + + const typeObject = types.find(o => o.id === type) || { + id: type, + name: type, + }; + + return ( + + ); +}; + +FeatureToggleListItemChip.propTypes = { + type: PropTypes.string.isRequired, + types: PropTypes.array, + onClick: PropTypes.func, +}; + +export default memo(FeatureToggleListItemChip); diff --git a/frontend/src/component/feature/list/feature-type-container.jsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItemChip/index.jsx similarity index 79% rename from frontend/src/component/feature/list/feature-type-container.jsx rename to frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItemChip/index.jsx index 523506ed82..a148ec6e07 100644 --- a/frontend/src/component/feature/list/feature-type-container.jsx +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItemChip/index.jsx @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import Component from './feature-type-component'; +import Component from './FeatureToggleListItemChip'; const mapStateToProps = state => ({ types: state.featureTypes.toJS(), diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItemChip/styles.js b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItemChip/styles.js new file mode 100644 index 0000000000..7873aeece8 --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/FeatureToggleListItemChip/styles.js @@ -0,0 +1,9 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + typeChip: { + margin: '0 8px', + boxShadow: theme.boxShadows.chip.main, + backgroundColor: theme.palette.chips.main, + }, +})); diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/index.jsx b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/index.jsx new file mode 100644 index 0000000000..600ff78b9e --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/index.jsx @@ -0,0 +1,3 @@ +import FeatureToggleListItem from './FeatureToggleListItem'; + +export default FeatureToggleListItem; diff --git a/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/styles.js b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/styles.js new file mode 100644 index 0000000000..4cf20d372b --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/FeatureToggleListItem/styles.js @@ -0,0 +1,23 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + listItem: { + padding: '0', + margin: '0.25rem 0', + }, + listItemMetric: { + width: '40px', + marginRight: '1rem', + flexShrink: '0', + }, + listItemSvg: { + fill: theme.palette.icons.lightGrey, + }, + listItemLink: { + marginLeft: '10px', + minWidth: '0', + }, + listItemStrategies: { + marginLeft: 'auto', + }, +})); diff --git a/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap b/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap new file mode 100644 index 0000000000..ae9d224fc4 --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap @@ -0,0 +1,193 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly with one feature 1`] = ` +
  • + + + + + + + + + + + + + + + + + + + + Another +   + + + + +
    + + another's description + +
    +
    +
    + + +
  • +`; + +exports[`renders correctly with one feature without permission 1`] = ` +
  • + + + + + + + + + + + + + + + + + + + + Another +   + + + + +
    + + another's description + +
    +
    +
    + + +
  • +`; diff --git a/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/list-component-test.jsx.snap b/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/list-component-test.jsx.snap new file mode 100644 index 0000000000..0e88bc6110 --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/__tests__/__snapshots__/list-component-test.jsx.snap @@ -0,0 +1,381 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly with one feature 1`] = ` +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +

    + Feature toggles +

    +
    +
    +
    +
    + + +
    + + + Create feature toggle + + +
    +
    +
    +
    +
    +
      + +
    +
    +
    +
    +`; + +exports[`renders correctly with one feature without permissions 1`] = ` +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +

    + Feature toggles +

    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
      + +
    +
    +
    +
    +`; diff --git a/frontend/src/component/feature/list/__tests__/feature-list-item-component-test.jsx b/frontend/src/component/feature/FeatureToggleList/__tests__/feature-list-item-component-test.jsx similarity index 56% rename from frontend/src/component/feature/list/__tests__/feature-list-item-component-test.jsx rename to frontend/src/component/feature/FeatureToggleList/__tests__/feature-list-item-component-test.jsx index e8d868d95b..d2f43ddc98 100644 --- a/frontend/src/component/feature/list/__tests__/feature-list-item-component-test.jsx +++ b/frontend/src/component/feature/FeatureToggleList/__tests__/feature-list-item-component-test.jsx @@ -1,12 +1,14 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { ThemeProvider } from '@material-ui/core'; -import Feature from '../list-item-component'; +import FeatureToggleListItem from '../FeatureToggleListItem'; import renderer from 'react-test-renderer'; import { UPDATE_FEATURE } from '../../../../permissions'; -jest.mock('react-mdl'); -jest.mock('../feature-type-container'); +import theme from '../../../../themes/main-theme'; + +jest.mock('../FeatureToggleListItem/FeatureToggleListItemChip'); test('renders correctly with one feature', () => { const feature = { @@ -28,15 +30,17 @@ test('renders correctly with one feature', () => { const settings = { sort: 'name' }; const tree = renderer.create( - permission === UPDATE_FEATURE} - /> + + permission === UPDATE_FEATURE} + /> + ); @@ -63,15 +67,17 @@ test('renders correctly with one feature without permission', () => { const settings = { sort: 'name' }; const tree = renderer.create( - false} - /> + + false} + /> + ); diff --git a/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx b/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx new file mode 100644 index 0000000000..1c3e6fa801 --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/__tests__/list-component-test.jsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { ThemeProvider } from '@material-ui/core'; + +import FeatureToggleList from '../FeatureToggleList'; +import renderer from 'react-test-renderer'; +import { CREATE_FEATURE } from '../../../../permissions'; +import theme from '../../../../themes/main-theme'; + +jest.mock('../FeatureToggleListItem', () => ({ + __esModule: true, + default: 'ListItem', +})); + +jest.mock('../../../common/ProjectSelect'); + +test('renders correctly with one feature', () => { + const features = [ + { + name: 'Another', + }, + ]; + const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} }; + const settings = { sort: 'name' }; + const tree = renderer.create( + + + permission === CREATE_FEATURE} + /> + + + ); + + expect(tree).toMatchSnapshot(); +}); + +test('renders correctly with one feature without permissions', () => { + const features = [ + { + name: 'Another', + }, + ]; + const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} }; + const settings = { sort: 'name' }; + const tree = renderer.create( + + + false} + /> + + + ); + + expect(tree).toMatchSnapshot(); +}); diff --git a/frontend/src/component/feature/list/list-container.jsx b/frontend/src/component/feature/FeatureToggleList/index.jsx similarity index 92% rename from frontend/src/component/feature/list/list-container.jsx rename to frontend/src/component/feature/FeatureToggleList/index.jsx index 836e85d12a..0039ad6989 100644 --- a/frontend/src/component/feature/list/list-container.jsx +++ b/frontend/src/component/feature/FeatureToggleList/index.jsx @@ -1,8 +1,8 @@ import { connect } from 'react-redux'; import { toggleFeature, fetchFeatureToggles } from '../../../store/feature-toggle/actions'; import { updateSettingForGroup } from '../../../store/settings/actions'; +import FeatureToggleList from './FeatureToggleList'; -import FeatureListComponent from './list-component'; import { hasPermission } from '../../../permissions'; function checkConstraints(strategy, regex) { @@ -99,15 +99,16 @@ export const mapStateToPropsConfigurable = isFeature => state => { featureMetrics, settings, hasPermission: hasPermission.bind(null, state.user.get('profile')), + loading: state.apiCalls.fetchTogglesState.loading, }; }; const mapStateToProps = mapStateToPropsConfigurable(true); const mapDispatchToProps = { toggleFeature, - fetchFeatureToggles, + fetcher: () => fetchFeatureToggles(), updateSetting: updateSettingForGroup('feature'), }; -const FeatureListContainer = connect(mapStateToProps, mapDispatchToProps)(FeatureListComponent); +const FeatureToggleListContainer = connect(mapStateToProps, mapDispatchToProps)(FeatureToggleList); -export default FeatureListContainer; +export default FeatureToggleListContainer; diff --git a/frontend/src/component/feature/FeatureToggleList/loadingFeatures.js b/frontend/src/component/feature/FeatureToggleList/loadingFeatures.js new file mode 100644 index 0000000000..50855162a9 --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/loadingFeatures.js @@ -0,0 +1,134 @@ +const loadingFeatures = [ + { + createdAt: '2021-03-19T09:16:21.329Z', + description: '', + enabled: true, + lastSeenAt: '2021-03-24T10:46:38.036Z', + name: 'one', + project: 'default', + reviveName: 'cool-thing', + stale: true, + strategies: [], + variants: [], + type: 'release', + }, + { + createdAt: '2021-03-19T09:16:21.329Z', + description: '', + enabled: true, + lastSeenAt: '2021-03-24T10:46:38.036Z', + name: 'two', + project: 'default', + reviveName: 'cool-thing', + stale: true, + strategies: [], + variants: [], + type: 'release', + }, + { + createdAt: '2021-03-19T09:16:21.329Z', + description: '', + enabled: true, + lastSeenAt: '2021-03-24T10:46:38.036Z', + name: 'three', + project: 'default', + reviveName: 'cool-thing', + stale: true, + strategies: [], + variants: [], + type: 'release', + }, + { + createdAt: '2021-03-19T09:16:21.329Z', + description: '', + enabled: true, + lastSeenAt: '2021-03-24T10:46:38.036Z', + name: 'four', + project: 'default', + reviveName: 'cool-thing', + stale: true, + strategies: [], + variants: [], + type: 'release', + }, + { + createdAt: '2021-03-19T09:16:21.329Z', + description: '', + enabled: true, + lastSeenAt: '2021-03-24T10:46:38.036Z', + name: 'five', + project: 'default', + reviveName: 'cool-thing', + stale: true, + strategies: [], + variants: [], + type: 'release', + }, + { + createdAt: '2021-03-19T09:16:21.329Z', + description: '', + enabled: true, + lastSeenAt: '2021-03-24T10:46:38.036Z', + name: 'six', + project: 'default', + reviveName: 'cool-thing', + stale: true, + strategies: [], + variants: [], + type: 'release', + }, + { + createdAt: '2021-03-19T09:16:21.329Z', + description: '', + enabled: true, + lastSeenAt: '2021-03-24T10:46:38.036Z', + name: 'seven', + project: 'default', + reviveName: 'cool-thing', + stale: true, + strategies: [], + variants: [], + type: 'release', + }, + { + createdAt: '2021-03-19T09:16:21.329Z', + description: '', + enabled: true, + lastSeenAt: '2021-03-24T10:46:38.036Z', + name: 'eight', + project: 'default', + reviveName: 'cool-thing', + stale: true, + strategies: [], + variants: [], + type: 'release', + }, + { + createdAt: '2021-03-19T09:16:21.329Z', + description: '', + enabled: true, + lastSeenAt: '2021-03-24T10:46:38.036Z', + name: 'nine', + project: 'default', + reviveName: 'cool-thing', + stale: true, + strategies: [], + variants: [], + type: 'release', + }, + { + createdAt: '2021-03-19T09:16:21.329Z', + description: '', + enabled: true, + lastSeenAt: '2021-03-24T10:46:38.036Z', + name: 'ten', + project: 'default', + reviveName: 'cool-thing', + stale: true, + strategies: [], + variants: [], + type: 'release', + }, +]; + +export default loadingFeatures; diff --git a/frontend/src/component/feature/FeatureToggleList/styles.js b/frontend/src/component/feature/FeatureToggleList/styles.js new file mode 100644 index 0000000000..b89f4c0384 --- /dev/null +++ b/frontend/src/component/feature/FeatureToggleList/styles.js @@ -0,0 +1,14 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles({ + actionsContainer: { + display: 'flex', + alignItems: 'center', + }, + listParagraph: { + textAlign: 'center', + }, + searchBarContainer: { + marginBottom: '2rem', + }, +}); diff --git a/frontend/src/component/feature/FeatureView/FeatureView.jsx b/frontend/src/component/feature/FeatureView/FeatureView.jsx new file mode 100644 index 0000000000..e85f956b2b --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureView.jsx @@ -0,0 +1,326 @@ +import React, { useEffect, useLayoutEffect, useState } from 'react'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import { Paper, Typography, Button, Switch, LinearProgress } from '@material-ui/core'; + +import HistoryComponent from '../../history/history-list-toggle-container'; +import MetricComponent from '../view/metric-container'; +import UpdateStrategies from '../view/update-strategies-container'; +import EditVariants from '../variant/update-variant-container'; +import FeatureTypeSelect from '../feature-type-select-container'; +import ProjectSelect from '../project-select-container'; +import UpdateDescriptionComponent from '../view/update-description-component'; +import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../../permissions'; +import StatusComponent from '../status-component'; +import FeatureTagComponent from '../feature-tag-component'; +import StatusUpdateComponent from '../view/status-update-component'; +import AddTagDialog from '../add-tag-dialog-container'; +import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; +import TabNav from '../../common/TabNav'; + +import { scrollToTop } from '../../common/util'; + +import styles from './FeatureView.module.scss'; +import ConfirmDialogue from '../../common/Dialogue'; + +import { useCommonStyles } from '../../../common.styles'; + +const FeatureView = ({ + activeTab, + featureToggleName, + features, + toggleFeature, + setStale, + removeFeatureToggle, + revive, + fetchArchive, + fetchFeatureToggles, + editFeatureToggle, + featureToggle, + history, + hasPermission, + untagFeature, + featureTags, + fetchTags, + tagTypes, +}) => { + const isFeatureView = !!fetchFeatureToggles; + const [delDialog, setDelDialog] = useState(false); + const commonStyles = useCommonStyles(); + + useEffect(() => { + scrollToTop(); + fetchTags(featureToggleName); + }, []); + + useLayoutEffect(() => { + if (features.length === 0) { + if (isFeatureView) { + fetchFeatureToggles(); + } else { + fetchArchive(); + } + } + }, [features]); + + const getTabComponent = key => { + switch (key) { + case 'activation': + if (isFeatureView && hasPermission(UPDATE_FEATURE)) { + return ; + } + return ( + + ); + case 'metrics': + return ; + case 'variants': + return ( + + ); + case 'log': + return ; + } + }; + const getTabData = () => [ + { + label: 'Activation', + component: getTabComponent('activation'), + name: 'strategies', + path: `/features/strategies/${featureToggleName}`, + }, + { + label: 'Metrics', + component: getTabComponent('metrics'), + name: 'metrics', + path: `/features/metrics/${featureToggleName}`, + }, + { + label: 'Variants', + component: getTabComponent('variants'), + name: 'variants', + path: `/features/variants/${featureToggleName}`, + }, + { + label: 'Log', + component: getTabComponent('log'), + name: 'logs', + path: `/features/logs/${featureToggleName}`, + }, + ]; + + if (!featureToggle) { + if (features.length === 0) { + return ; + } + return ( + + Could not find the toggle{' '} + + {featureToggleName} + + } + elseShow={featureToggleName} + /> + + ); + } + + const removeToggle = () => { + removeFeatureToggle(featureToggle.name); + history.push('/features'); + }; + const reviveToggle = () => { + revive(featureToggle.name); + history.push('/features'); + }; + const updateDescription = description => { + let feature = { ...featureToggle, description }; + if (Array.isArray(feature.strategies)) { + feature.strategies.forEach(s => { + delete s.id; + }); + } + + editFeatureToggle(feature); + }; + const updateType = evt => { + evt.preventDefault(); + const type = evt.target.value; + let feature = { ...featureToggle, type }; + if (Array.isArray(feature.strategies)) { + feature.strategies.forEach(s => { + delete s.id; + }); + } + + editFeatureToggle(feature); + }; + + const updateProject = evt => { + evt.preventDefault(); + const project = evt.target.value; + let feature = { ...featureToggle, project }; + if (Array.isArray(feature.strategies)) { + feature.strategies.forEach(s => { + delete s.id; + }); + } + + editFeatureToggle(feature); + }; + + const updateStale = stale => { + setStale(stale, featureToggleName); + }; + + const tabs = getTabData(); + + const findActiveTab = activeTab => tabs.findIndex(tab => tab.name === activeTab); + return ( + +
    +
    + + {featureToggle.name} + + +
    +
    + +
    + +   + +
    + +
    +
    + +
    + + + toggleFeature(!featureToggle.enabled, featureToggle.name)} + /> + {featureToggle.enabled ? 'Enabled' : 'Disabled'} + + } + elseShow={ + <> + + {featureToggle.enabled ? 'Enabled' : 'Disabled'} + + } + /> + + + + + + + + +
    + } + elseShow={ + + } + /> + + +
    + + + { + setDelDialog(false); + removeToggle(); + }} + /> +
    + ); +}; + +FeatureView.propTypes = { + activeTab: PropTypes.string.isRequired, + featureToggleName: PropTypes.string.isRequired, + features: PropTypes.array.isRequired, + toggleFeature: PropTypes.func, + setStale: PropTypes.func, + removeFeatureToggle: PropTypes.func, + revive: PropTypes.func, + fetchArchive: PropTypes.func, + fetchFeatureToggles: PropTypes.func, + fetchFeatureToggle: PropTypes.func, + editFeatureToggle: PropTypes.func, + featureToggle: PropTypes.object, + history: PropTypes.object.isRequired, + hasPermission: PropTypes.func.isRequired, + fetchTags: PropTypes.func, + untagFeature: PropTypes.func, + featureTags: PropTypes.array, + tagTypes: PropTypes.array, +}; + +export default FeatureView; diff --git a/frontend/src/component/feature/FeatureView/FeatureView.module.scss b/frontend/src/component/feature/FeatureView/FeatureView.module.scss new file mode 100644 index 0000000000..2ef31b9ab8 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureView.module.scss @@ -0,0 +1,32 @@ +.header { + display: flex; + padding: var(--card-header-padding); + justify-content: space-between; + align-items: center; + border-bottom: var(--default-border); +} + +.heading { + font-size: var(--h1-size); +} + +.featureInfoContainer { + padding: var(--card-header-padding); +} + +.selectContainer { + margin-top: 1rem; +} + +.actions { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--card-header-padding); + border-bottom: var(--default-border); + border-top: var(--default-border); +} + +.tabContentContainer { + padding: var(--card-padding); +} diff --git a/frontend/src/component/feature/view/view-container.jsx b/frontend/src/component/feature/FeatureView/index.jsx similarity index 88% rename from frontend/src/component/feature/view/view-container.jsx rename to frontend/src/component/feature/FeatureView/index.jsx index 7507a7c2b9..67d3da2ba8 100644 --- a/frontend/src/component/feature/view/view-container.jsx +++ b/frontend/src/component/feature/FeatureView/index.jsx @@ -7,9 +7,9 @@ import { setStale, removeFeatureToggle, editFeatureToggle, -} from './../../../store/feature-toggle/actions'; +} from '../../../store/feature-toggle/actions'; -import ViewToggleComponent from './view-component'; +import FeatureView from './FeatureView'; import { hasPermission } from '../../../permissions'; import { fetchTags, tagFeature, untagFeature } from '../../../store/feature-tags/actions'; @@ -33,4 +33,4 @@ export default connect( untagFeature, fetchTags, } -)(ViewToggleComponent); +)(FeatureView); diff --git a/frontend/src/component/feature/__tests__/__snapshots__/progress-test.jsx.snap b/frontend/src/component/feature/__tests__/__snapshots__/progress-test.jsx.snap index d36e393cdf..4ff1c90119 100644 --- a/frontend/src/component/feature/__tests__/__snapshots__/progress-test.jsx.snap +++ b/frontend/src/component/feature/__tests__/__snapshots__/progress-test.jsx.snap @@ -5,7 +5,7 @@ exports[`renders correctly with 0% done no fallback 1`] = ` viewBox="0 0 100 100" > `; @@ -58,7 +57,7 @@ exports[`renders correctly with 15% done no fallback 1`] = ` viewBox="0 0 100 100" > `; diff --git a/frontend/src/component/feature/__tests__/progress-test.jsx b/frontend/src/component/feature/__tests__/progress-test.jsx index 8c8f67f8ff..5cd49b0128 100644 --- a/frontend/src/component/feature/__tests__/progress-test.jsx +++ b/frontend/src/component/feature/__tests__/progress-test.jsx @@ -3,7 +3,7 @@ import React from 'react'; import Progress from '../progress-component'; import renderer from 'react-test-renderer'; -jest.mock('react-mdl'); +jest.mock('@material-ui/core'); test('renders correctly with 15% done no fallback', () => { const percent = 15; diff --git a/frontend/src/component/feature/add-tag-dialog-component.jsx b/frontend/src/component/feature/add-tag-dialog-component.jsx index 957d6a1df8..cde2a5a453 100644 --- a/frontend/src/component/feature/add-tag-dialog-component.jsx +++ b/frontend/src/component/feature/add-tag-dialog-component.jsx @@ -1,10 +1,12 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import Modal from 'react-modal'; -import { Button, Textfield, DialogActions } from 'react-mdl'; -import { FormButtons } from '../common'; + +import { DialogContentText, Button, TextField } from '@material-ui/core'; import TagTypeSelect from '../feature/tag-type-select-container'; -import { trim, modalStyles } from '../common/util'; +import Dialogue from '../common/Dialogue'; +import { trim } from '../common/util'; + +import styles from './add-tag-dialog-component.module.scss'; class AddTagDialogComponent extends Component { constructor(props) { @@ -54,44 +56,43 @@ class AddTagDialogComponent extends Component { }; render() { const { tag, errors, openDialog } = this.state; - const submitText = 'Tag feature'; return ( - - Add tag + + -

    {submitText}

    -

    Tags allows you to group features together

    -
    -
    - this.setValue('type', v.target.value)} - /> -
    - this.setValue('value', v.target.value)} - /> -
    - {errors.general &&

    {errors.general}

    } - - - -
    -
    + <> + Tags allows you to group features together +
    +
    + this.setValue('type', v.target.value)} + /> +
    + this.setValue('value', v.target.value)} + /> +
    + {errors.general &&

    {errors.general}

    } +
    + +
    ); } diff --git a/frontend/src/component/feature/add-tag-dialog-component.module.scss b/frontend/src/component/feature/add-tag-dialog-component.module.scss new file mode 100644 index 0000000000..73f0c9063a --- /dev/null +++ b/frontend/src/component/feature/add-tag-dialog-component.module.scss @@ -0,0 +1,3 @@ +.dialogueFormContent > * { + margin: 0.5rem 0; +} \ No newline at end of file diff --git a/frontend/src/component/feature/create/__tests__/__snapshots__/add-feature-component-test.jsx.snap b/frontend/src/component/feature/create/__tests__/__snapshots__/add-feature-component-test.jsx.snap deleted file mode 100644 index 1e7d6ede48..0000000000 --- a/frontend/src/component/feature/create/__tests__/__snapshots__/add-feature-component-test.jsx.snap +++ /dev/null @@ -1,105 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render the create feature page 1`] = ` - - - Create new feature toggle - -
    - - - - - - - - -
    - -
    -
    - -
    -
    - - Disabled - feature toggle - -
    -
    - -
    - - - -
    -
    -`; diff --git a/frontend/src/component/feature/create/__tests__/add-feature-component-test.jsx b/frontend/src/component/feature/create/__tests__/add-feature-component-test.jsx deleted file mode 100644 index 5be323e331..0000000000 --- a/frontend/src/component/feature/create/__tests__/add-feature-component-test.jsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from 'react'; -import AddFeatureComponent from '../add-feature-component'; -import { shallow } from 'enzyme/build'; - -jest.mock('react-mdl'); - -it('render the create feature page', () => { - let input = { - name: 'feature', - description: 'Description', - enabled: false, - }; - let errors = {}; - const tree = shallow( - - ); - expect(tree).toMatchSnapshot(); -}); - -let input = { - name: 'feature', - description: 'Description', - enabled: false, -}; -let errors = {}; - -let validateName = jest.fn(); -let setValue = jest.fn(); -let onSubmit = jest.fn(); -let addStrategy = jest.fn(); -let removeStrategy = jest.fn(); -let moveStrategy = jest.fn(); -let onCancel = jest.fn(); -let updateStratgy = jest.fn(); -let init = jest.fn(); -let eventMock = { - preventDefault: () => {}, - stopPropagation: () => {}, - target: { - name: 'NAME', - }, -}; -const buildComponent = (setValue, validateName) => ( - -); - -it('add a feature name with validation', () => { - let called = false; - validateName = () => { - called = true; - }; - const wrapper = shallow(buildComponent(setValue, validateName)); - wrapper - .find('react-mdl-Textfield') - .first() - .simulate('blur', eventMock); - expect(called).toBe(true); -}); - -it('set a value for feature name', () => { - let called = false; - setValue = () => { - called = true; - }; - let wrapper = shallow(buildComponent(setValue, validateName)); - wrapper - .find('react-mdl-Textfield') - .first() - .simulate('change', eventMock); - expect(called).toBe(true); -}); - -it('set a description for feature name', () => { - let called = false; - setValue = () => { - called = true; - }; - let wrapper = shallow(buildComponent(setValue, validateName)); - wrapper - .find('react-mdl-Textfield') - .last() - .simulate('change', eventMock); - expect(called).toBe(true); -}); - -it('switch the toggle', () => { - let called = false; - setValue = () => { - called = true; - }; - let wrapper = shallow(buildComponent(setValue, validateName)); - eventMock.target.enabled = false; - wrapper - .find('react-mdl-Switch') - .last() - .simulate('change', eventMock); - expect(called).toBe(true); -}); diff --git a/frontend/src/component/feature/create/add-feature-component.jsx b/frontend/src/component/feature/create/add-feature-component.jsx index 753190fc9c..900d4466a9 100644 --- a/frontend/src/component/feature/create/add-feature-component.jsx +++ b/frontend/src/component/feature/create/add-feature-component.jsx @@ -1,13 +1,17 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { Textfield, Switch, Card, CardTitle, CardActions, Grid, Cell } from 'react-mdl'; +import { CardActions, Switch, TextField } from '@material-ui/core'; import FeatureTypeSelect from '../feature-type-select-container'; import ProjectSelect from '../project-select-container'; -import StrategiesList from '../strategy/strategies-list-add-container'; +import StrategiesList from '../strategy/strategies-list-container'; +import PageContent from '../../common/PageContent/PageContent'; import { FormButtons, styles as commonStyles } from '../../common'; import { trim } from '../../common/util'; +import styles from './add-feature-component.module.scss'; +import { CF_CREATE_BTN_ID, CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from '../../../testIds'; + class AddFeatureComponent extends Component { // static displayName = `AddFeatureComponent-${getDisplayName(Component)}`; componentDidMount() { @@ -22,53 +26,69 @@ class AddFeatureComponent extends Component { const { input, errors, setValue, validateName, onSubmit, onCancel } = this.props; return ( - - Create new feature toggle +
    - - - validateName(v.target.value)} - onChange={v => setValue('name', trim(v.target.value))} - /> - - - setValue('type', v.target.value)} /> - - -
    +
    + validateName(v.target.value)} + onChange={v => setValue('name', trim(v.target.value))} + /> +
    +
    + setValue('type', v.target.value)} + label={'Toggle type'} + id="feature-type-select" + inputProps={{ + 'data-test': CF_TYPE_ID, + }} + /> +
    + +
    setValue('project', v.target.value)} />
    -
    - + setValue('description', v.target.value)} />
    -
    +
    { setValue('enabled', !input.enabled); }} - > - {input.enabled ? 'Enabled' : 'Disabled'} feature toggle - + /> +

    {input.enabled ? 'Enabled' : 'Disabled'} feature toggle

    -
    +
    - + - + ); } } diff --git a/frontend/src/component/feature/create/add-feature-component.module.scss b/frontend/src/component/feature/create/add-feature-component.module.scss new file mode 100644 index 0000000000..202e2f55d6 --- /dev/null +++ b/frontend/src/component/feature/create/add-feature-component.module.scss @@ -0,0 +1,30 @@ +.header { + font-size: var(--h1-size); + padding: var(--card-header-padding); +} + +.container { + padding: var(--card-padding); +} + +.nameInput { + margin-right: 1.5rem; +} + +.formContainer { + margin-bottom: 1.5rem; + max-width: 350px; +} + +.legalValueFormContainer { + display: flex; +} + +.toggleContainer { + display: flex; + align-items: center; +} + +.strategiesContainer { + margin: 2rem 0; +} diff --git a/frontend/src/component/feature/create/copy-feature-component.jsx b/frontend/src/component/feature/create/copy-feature-component.jsx index aec2f91bf9..e83ec7f5cb 100644 --- a/frontend/src/component/feature/create/copy-feature-component.jsx +++ b/frontend/src/component/feature/create/copy-feature-component.jsx @@ -3,9 +3,10 @@ import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; -import { Button, Icon, Textfield, Checkbox, Card, CardTitle, CardActions } from 'react-mdl'; +import { Button, Icon, TextField, Switch, Paper, FormControlLabel } from '@material-ui/core'; import { styles as commonStyles } from '../../common'; +import styles from './copy-feature-component.module.scss'; import { trim } from '../../common/util'; @@ -15,6 +16,7 @@ class CopyFeatureComponent extends Component { constructor() { super(); this.state = { newToggleName: '', replaceGroupId: true }; + this.inputRef = React.createRef(); } // eslint-disable-next-line camelcase @@ -27,7 +29,7 @@ class CopyFeatureComponent extends Component { componentDidMount() { if (this.props.copyToggle) { - this.refs.name.inputRef.focus(); + this.inputRef.current.focus(); } else { this.props.fetchFeatureToggles(); } @@ -84,47 +86,51 @@ class CopyFeatureComponent extends Component { const { newToggleName, nameError, replaceGroupId } = this.state; return ( - - - Copy {copyToggle.name} - + +
    +

    Copy {copyToggle.name}

    +
    -
    -
    -

    - You are about to create a new feature toggle by cloning the configuration of feature - toggle  - {copyToggle.name}. You must give - the new feature toggle a unique name before you can proceed. -

    - +

    + You are about to create a new feature toggle by cloning the configuration of feature + toggle  + {copyToggle.name}. You must give the + new feature toggle a unique name before you can proceed. +

    + + -
    -
    - + } label="Replace groupId" - onChange={this.toggleReplaceGroupId} /> -
    -
    - - -
    -
    -
    -
    + +
    + ); } } diff --git a/frontend/src/component/feature/create/copy-feature-component.module.scss b/frontend/src/component/feature/create/copy-feature-component.module.scss new file mode 100644 index 0000000000..4fccdbb2eb --- /dev/null +++ b/frontend/src/component/feature/create/copy-feature-component.module.scss @@ -0,0 +1,28 @@ +.header { + padding: var(--card-header-padding); + border: var(--default-border); +} + +.header h1 { + font-size: var(--h1-size); +} + +.content { + padding: var(--card-padding); + +} + +.content form { + display: flex; + flex-direction: column; + max-width: 400px; +} + +.content > *, +.content form > * { + margin: 1rem 0; +} + +.text { + max-width: 400px; +} \ No newline at end of file diff --git a/frontend/src/component/feature/feature-tag-component.jsx b/frontend/src/component/feature/feature-tag-component.jsx index c8308d597e..18941ba61f 100644 --- a/frontend/src/component/feature/feature-tag-component.jsx +++ b/frontend/src/component/feature/feature-tag-component.jsx @@ -1,28 +1,32 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Chip, ChipContact, Icon } from 'react-mdl'; +import { Icon, Chip } from '@material-ui/core'; +import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; +import Dialogue from '../common/Dialogue'; function FeatureTagComponent({ tags, tagTypes, featureToggleName, untagFeature }) { + const [showDialog, setShowDialog] = useState(false); + const [selectedTag, setSelectedTag] = useState(undefined); const onUntagFeature = tag => { // eslint-disable-next-line no-alert - if (window.confirm('Are you sure you want to remove this tag')) { - untagFeature(featureToggleName, tag); - } + untagFeature(featureToggleName, tag); + setSelectedTag(undefined); }; const tagIcon = typeName => { let tagType = tagTypes.find(type => type.name === typeName); - const style = { width: '20px', height: '20px', margin: '0' }; + + const style = { width: '20px', height: '20px', marginRight: '5px' }; if (tagType && tagType.icon) { switch (tagType.name) { case 'slack': - return ; + return slack; case 'jira': - return ; + return jira; case 'webhook': - return ; + return webhook; default: - return ; + return label; } } else { return {typeName[0].toUpperCase()}; @@ -31,22 +35,40 @@ function FeatureTagComponent({ tags, tagTypes, featureToggleName, untagFeature } const renderTag = t => ( onUntagFeature(t)} - title={`Type: ${t.type} \nValue: ${t.value}`} - key={`${t.type}:${t.value}`} + icon={tagIcon(t.type)} style={{ marginRight: '3px', fontSize: '0.8em' }} - > - {tagIcon(t.type)} - {t.value} - + label={t.value} + key={`${t.type}:${t.value}`} + onDelete={() => { + setSelectedTag(t); + setShowDialog(true); + }} + /> ); - return tags && tags.length > 0 ? ( -
    -

    Tags

    - {tags.map(renderTag)} -
    - ) : null; + return ( + 0} + show={ +
    + { + setShowDialog(false); + setSelectedTag(undefined); + }} + onClick={() => { + onUntagFeature(selectedTag); + setShowDialog(false); + }} + title="Are you sure you want to delete this tag?" + /> +

    Tags

    + {tags.map(renderTag)} +
    + } + /> + ); } FeatureTagComponent.propTypes = { diff --git a/frontend/src/component/feature/feature-type-select-component.jsx b/frontend/src/component/feature/feature-type-select-component.jsx index ddbee02f73..3cee07003b 100644 --- a/frontend/src/component/feature/feature-type-select-component.jsx +++ b/frontend/src/component/feature/feature-type-select-component.jsx @@ -11,15 +11,28 @@ class FeatureTypeSelectComponent extends Component { } render() { - const { value, types, onChange, filled } = this.props; + const { + value, + types, + onChange, + label, + id, + // eslint-disable-next-line no-unused-vars + fetchFeatureTypes, + ...rest + } = this.props; - const options = types.map(t => ({ key: t.id, label: t.name, title: t.description })); + const options = types.map(t => ({ + key: t.id, + label: t.name, + title: t.description, + })); if (!options.find(o => o.key === value)) { options.push({ key: value, label: value }); } - return ; + return ; } } @@ -29,6 +42,8 @@ FeatureTypeSelectComponent.propTypes = { types: PropTypes.array.isRequired, fetchFeatureTypes: PropTypes.func, onChange: PropTypes.func.isRequired, + label: PropTypes.string, + id: PropTypes.string, }; export default FeatureTypeSelectComponent; diff --git a/frontend/src/component/feature/list/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap b/frontend/src/component/feature/list/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap deleted file mode 100644 index 4993962a75..0000000000 --- a/frontend/src/component/feature/list/__tests__/__snapshots__/feature-list-item-component-test.jsx.snap +++ /dev/null @@ -1,138 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders correctly with one feature 1`] = ` - - - - - - - - - - - - - Another -   - - - - -
    - - another's description - -
    -
    -
    - - -
    -`; - -exports[`renders correctly with one feature without permission 1`] = ` - - - - - - - - - - - - - Another -   - - - - -
    - - another's description - -
    -
    -
    - - -
    -`; diff --git a/frontend/src/component/feature/list/__tests__/__snapshots__/list-component-test.jsx.snap b/frontend/src/component/feature/list/__tests__/__snapshots__/list-component-test.jsx.snap deleted file mode 100644 index 20857e067d..0000000000 --- a/frontend/src/component/feature/list/__tests__/__snapshots__/list-component-test.jsx.snap +++ /dev/null @@ -1,418 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders correctly with one feature 1`] = ` -
    -
    -
    - - - - -
    - - - - - -
    - - - - Last minute - - - - - - Last minute - - - - Last hour - - - - By name - - - - - Name - - - Type - - - Enabled - - - Stale - - - Created - - - Last seen - - - Strategies - - - Metrics - - - - -
    - - - -
    -
    -`; - -exports[`renders correctly with one feature without permissions 1`] = ` -
    -
    -
    - - - - -
    - -
    - - - - Last minute - - - - - - Last minute - - - - Last hour - - - - By name - - - - - Name - - - Type - - - Enabled - - - Stale - - - Created - - - Last seen - - - Strategies - - - Metrics - - - - -
    - - - -
    -
    -`; diff --git a/frontend/src/component/feature/list/__tests__/list-component-test.jsx b/frontend/src/component/feature/list/__tests__/list-component-test.jsx deleted file mode 100644 index 25aee564d1..0000000000 --- a/frontend/src/component/feature/list/__tests__/list-component-test.jsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; - -import FeatureListComponent from './../list-component'; -import renderer from 'react-test-renderer'; -import { CREATE_FEATURE } from '../../../../permissions'; - -jest.mock('react-mdl'); -jest.mock('../list-item-component', () => ({ - __esModule: true, - default: 'ListItem', -})); - -jest.mock('../project-container', () => 'Project'); - -test('renders correctly with one feature', () => { - const features = [ - { - name: 'Another', - }, - ]; - const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} }; - const settings = { sort: 'name' }; - const tree = renderer.create( - - permission === CREATE_FEATURE} - /> - - ); - - expect(tree).toMatchSnapshot(); -}); - -test('renders correctly with one feature without permissions', () => { - const features = [ - { - name: 'Another', - }, - ]; - const featureMetrics = { lastHour: {}, lastMinute: {}, seenApps: {} }; - const settings = { sort: 'name' }; - const tree = renderer.create( - - false} - /> - - ); - - expect(tree).toMatchSnapshot(); -}); diff --git a/frontend/src/component/feature/list/feature-type-component.jsx b/frontend/src/component/feature/list/feature-type-component.jsx deleted file mode 100644 index 4000f96fd1..0000000000 --- a/frontend/src/component/feature/list/feature-type-component.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import React, { memo } from 'react'; -import { Chip } from 'react-mdl'; -import PropTypes from 'prop-types'; -import styles from './list.module.scss'; - -function StatusComponent({ type, types, onClick }) { - const typeObject = types.find(o => o.id === type) || { id: type, name: type }; - - return ( - - {typeObject.name} - - ); -} - -export default memo(StatusComponent); - -StatusComponent.propTypes = { - type: PropTypes.string.isRequired, - types: PropTypes.array, - onClick: PropTypes.func, -}; diff --git a/frontend/src/component/feature/list/list-component.jsx b/frontend/src/component/feature/list/list-component.jsx deleted file mode 100644 index ace6bffc6f..0000000000 --- a/frontend/src/component/feature/list/list-component.jsx +++ /dev/null @@ -1,158 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { debounce } from 'debounce'; -import { Link } from 'react-router-dom'; -import { Icon, FABButton, Menu, MenuItem, Card, CardActions, List } from 'react-mdl'; -import Feature from './list-item-component'; -import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../../common'; -import SearchField from '../../common/search-field'; -import { CREATE_FEATURE } from '../../../permissions'; -import ProjectMenu from './project-container'; - -export default class FeatureListComponent extends React.Component { - static propTypes = { - features: PropTypes.array.isRequired, - featureMetrics: PropTypes.object.isRequired, - fetchFeatureToggles: PropTypes.func, - fetchArchive: PropTypes.func, - revive: PropTypes.func, - updateSetting: PropTypes.func.isRequired, - toggleFeature: PropTypes.func, - settings: PropTypes.object, - history: PropTypes.object.isRequired, - hasPermission: PropTypes.func.isRequired, - }; - - constructor(props) { - super(); - this.state = { - filter: props.settings.filter, - updateFilter: debounce(props.updateSetting.bind(this, 'filter'), 150), - }; - } - - componentDidMount() { - if (this.props.fetchFeatureToggles) { - this.props.fetchFeatureToggles(); - } else { - this.props.fetchArchive(); - } - } - - toggleMetrics() { - this.props.updateSetting('showLastHour', !this.props.settings.showLastHour); - } - - setFilter = v => { - const value = typeof v === 'string' ? v : ''; - this.setState({ filter: value }); - this.state.updateFilter(value); - }; - - setSort(v) { - this.props.updateSetting('sort', typeof v === 'string' ? v.trim() : ''); - } - - render() { - const { features, toggleFeature, featureMetrics, settings, revive, hasPermission } = this.props; - features.forEach(e => { - e.reviveName = e.name; - }); - return ( -
    -
    - - {hasPermission(CREATE_FEATURE) ? ( - - - - - - ) : ( - '' - )} -
    - - - - this.toggleMetrics()} style={{ width: '168px' }}> - - - - - this.setSort(e.target.getAttribute('data-target'))} - style={{ width: '168px' }} - > - - Name - - - Type - - - Enabled - - - Stale - - - Created - - - Last seen - - - Strategies - - - Metrics - - - - -
    - - {features.length > 0 ? ( - features.map((feature, i) => ( - - )) - ) : ( -

    - Empty list of feature toggles -

    - )} -
    -
    -
    - ); - } -} diff --git a/frontend/src/component/feature/list/list-item-component.jsx b/frontend/src/component/feature/list/list-item-component.jsx deleted file mode 100644 index e85db6c43c..0000000000 --- a/frontend/src/component/feature/list/list-item-component.jsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { memo } from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; -import { Switch, ListItem, ListItemAction, Icon } from 'react-mdl'; -import TimeAgo from 'react-timeago'; -import Progress from '../progress-component'; -import { UPDATE_FEATURE } from '../../../permissions'; -import { calc, styles as commonStyles } from '../../common'; -import Status from '../status-component'; -import FeatureType from './feature-type-container'; - -import styles from './list.module.scss'; - -const Feature = ({ - feature, - toggleFeature, - settings, - metricsLastHour = { yes: 0, no: 0, isFallback: true }, - metricsLastMinute = { yes: 0, no: 0, isFallback: true }, - revive, - hasPermission, -}) => { - const { name, description, enabled, type, stale, createdAt } = feature; - const { showLastHour = false } = settings; - const isStale = showLastHour ? metricsLastHour.isFallback : metricsLastMinute.isFallback; - const percent = - 1 * - (showLastHour - ? calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0) - : calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0)); - const featureUrl = toggleFeature === undefined ? `/archive/strategies/${name}` : `/features/strategies/${name}`; - return ( - - - - - - {hasPermission(UPDATE_FEATURE) ? ( - toggleFeature(!enabled, name)} - checked={enabled} - /> - ) : ( - - )} - - - - {name}  - - - -
    - {description} -
    - -
    - - - - - {revive && hasPermission(UPDATE_FEATURE) ? ( - revive(feature.name)}> - - - ) : ( - - )} -
    - ); -}; - -Feature.propTypes = { - feature: PropTypes.object, - toggleFeature: PropTypes.func, - settings: PropTypes.object, - metricsLastHour: PropTypes.object, - metricsLastMinute: PropTypes.object, - revive: PropTypes.func, - hasPermission: PropTypes.func.isRequired, -}; - -export default memo(Feature); diff --git a/frontend/src/component/feature/list/list.module.scss b/frontend/src/component/feature/list/list.module.scss deleted file mode 100644 index 4d7ec5236c..0000000000 --- a/frontend/src/component/feature/list/list.module.scss +++ /dev/null @@ -1,29 +0,0 @@ -.listItemMetric { - width: 40px; - flex-shrink: 0; - margin-right: 16px; -} - -.listItemToggle { - width: 36px; - flex-shrink: 0; - margin-right: 16px; -} - -.listItemLink { - min-width: 0; -} - -.listItemStrategies { - flex-shrink: 0; -} - -.strategyChip { - margin-left: 8px !important; -} - -.typeChip { - margin: 0 8px !important; - box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12); - background-color: #b0bec5 !important; -} \ No newline at end of file diff --git a/frontend/src/component/feature/list/project-component.jsx b/frontend/src/component/feature/list/project-component.jsx deleted file mode 100644 index ab1579fece..0000000000 --- a/frontend/src/component/feature/list/project-component.jsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useEffect } from 'react'; -import { Menu, MenuItem } from 'react-mdl'; -import { DropdownButton } from '../../common'; -import PropTypes from 'prop-types'; - -const ALL_PROJECTS = { id: '*', name: '> All projects' }; - -function projectItem(selectedId, item) { - return ( - - {item.name} - - ); -} - -function ProjectComponent({ projects, currentProjectId, updateCurrentProject, enabled, fetchProjects }) { - function setProject(v) { - const id = typeof v === 'string' ? v.trim() : ''; - updateCurrentProject(id); - } - - useEffect(() => { - if (enabled) { - fetchProjects(); - } - }, [enabled]); - - if (!enabled) { - return null; - } - - // TODO fixme - let currentProject = projects.find(i => i.id === currentProjectId); - if (!currentProject) { - currentProject = ALL_PROJECTS; - } - return ( - - - setProject(e.target.getAttribute('data-target'))} - style={{ width: '168px' }} - > - - {ALL_PROJECTS.name} - - {projects.map(p => projectItem(currentProjectId, p))} - - - ); -} - -ProjectComponent.propTypes = { - projects: PropTypes.array.isRequired, - fetchProjects: PropTypes.func.isRequired, - currentProjectId: PropTypes.string.isRequired, - updateCurrentProject: PropTypes.func.isRequired, - enabled: PropTypes.bool, -}; - -export default ProjectComponent; diff --git a/frontend/src/component/feature/progress-component.jsx b/frontend/src/component/feature/progress-component.jsx index 36286bb370..89bac30340 100644 --- a/frontend/src/component/feature/progress-component.jsx +++ b/frontend/src/component/feature/progress-component.jsx @@ -4,7 +4,7 @@ import styles from './progress.module.scss'; class Progress extends Component { constructor(props) { - super(); + super(props); this.state = { percentage: props.initialAnimation ? 0 : props.percentage, @@ -98,23 +98,18 @@ class Progress extends Component { }; return isFallback ? ( - + { // eslint-disable-next-line max-len } ) : ( - + ({ key: t.id, label: t.name, title: t.description })); + const options = projects.map(t => ({ + key: t.id, + label: t.name, + title: t.description, + })); - if (!options.find(o => o.key === value)) { + if (value && !options.find(o => o.key === value)) { options.push({ key: value, label: value }); } - return ; + return ; } } diff --git a/frontend/src/component/feature/status-component.jsx b/frontend/src/component/feature/status-component.jsx index a823538cc5..40f1b6f5e5 100644 --- a/frontend/src/component/feature/status-component.jsx +++ b/frontend/src/component/feature/status-component.jsx @@ -1,24 +1,23 @@ import React, { memo } from 'react'; -import { Chip } from 'react-mdl'; +import classnames from 'classnames'; +import { Chip } from '@material-ui/core'; import PropTypes from 'prop-types'; +import styles from './status.module.scss'; function StatusComponent({ stale, style, showActive = true }) { if (!stale && !showActive) { return null; } - const className = stale - ? 'mdl-color--red mdl-color-text--white mdl-shadow--2dp' - : 'mdl-color--light-green-500 mdl-color-text--white mdl-shadow--2dp'; + const className = classnames({ + [styles.stale]: stale, + [styles.active]: !stale, + }); const title = stale ? 'Feature toggle is deprecated.' : 'Feature toggle is active.'; const value = stale ? 'Stale' : 'Active'; - return ( - - {value} - - ); + return ; } export default memo(StatusComponent); diff --git a/frontend/src/component/feature/status.module.scss b/frontend/src/component/feature/status.module.scss new file mode 100644 index 0000000000..0128854812 --- /dev/null +++ b/frontend/src/component/feature/status.module.scss @@ -0,0 +1,14 @@ +.stale { + background-color: var(--danger); + color: #fff; +} + +.active { + background-color: var(--success); + color: #000; +} + +.stale, +.active { + box-shadow: var(--chip-shadow); +} diff --git a/frontend/src/component/feature/strategy/AddStrategy/AddStrategy.jsx b/frontend/src/component/feature/strategy/AddStrategy/AddStrategy.jsx new file mode 100644 index 0000000000..03fb13a1aa --- /dev/null +++ b/frontend/src/component/feature/strategy/AddStrategy/AddStrategy.jsx @@ -0,0 +1,96 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Dialog, DialogContent, DialogTitle, DialogActions, Typography } from '@material-ui/core'; + +import CreateStrategyCard from './AddStrategyCard/AddStrategyCard'; +import { useStyles } from './AddStrategy.styles'; +import ConditionallyRender from '../../../common/ConditionallyRender'; +import { resolveDefaultParamValue } from './utils'; +import { getHumanReadbleStrategy } from '../../../../utils/strategy-names'; + +const AddStrategy = ({ strategies, showCreateStrategy, setShowCreateStrategy, featureToggleName, addStrategy }) => { + const styles = useStyles(); + if (!strategies) return null; + + const builtInStrategies = strategies.filter(strategy => strategy.editable !== true); + const customStrategies = strategies.filter(strategy => strategy.editable); + + const setStrategyByName = strategyName => { + const selectedStrategy = strategies.find(s => s.name === strategyName); + const parameters = {}; + + selectedStrategy.parameters.forEach(({ name }) => { + parameters[name] = resolveDefaultParamValue(name, featureToggleName); + }); + + addStrategy({ + name: selectedStrategy.name, + parameters, + }); + }; + + const renderBuiltInStrategies = () => + builtInStrategies.map(strategy => ( + { + setShowCreateStrategy(false); + setStrategyByName(strategy.name); + }} + /> + )); + + const renderCustomStrategies = () => + customStrategies.map(strategy => ( + { + setShowCreateStrategy(false); + setStrategyByName(strategy.name); + }} + /> + )); + + return ( + + Add a new strategy + + + + Built in strategies + +
    {renderBuiltInStrategies()}
    + + 0} + show={ + <> + + Custom strategies + +
    {renderCustomStrategies()}
    + + } + /> + + + + + + + ); +}; + +AddStrategy.propTypes = { + strategies: PropTypes.array.isRequired, + showCreateStrategy: PropTypes.bool.isRequired, + setShowCreateStrategy: PropTypes.func.isRequired, + featureToggleName: PropTypes.string.isRequired, + addStrategy: PropTypes.func.isRequired, +}; + +export default AddStrategy; diff --git a/frontend/src/component/feature/strategy/AddStrategy/AddStrategy.styles.js b/frontend/src/component/feature/strategy/AddStrategy/AddStrategy.styles.js new file mode 100644 index 0000000000..10b03b30ec --- /dev/null +++ b/frontend/src/component/feature/strategy/AddStrategy/AddStrategy.styles.js @@ -0,0 +1,17 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + createStrategyCardContainer: { + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'center', + '& > *': { + marginRight: '0.5rem', + marginTop: '0.5rem', + }, + }, + subTitle: { + fontWeight: theme.fontWeight.semi, + margin: '1rem 0', + }, +})); diff --git a/frontend/src/component/feature/strategy/AddStrategy/AddStrategyCard/AddStrategyCard.jsx b/frontend/src/component/feature/strategy/AddStrategy/AddStrategyCard/AddStrategyCard.jsx new file mode 100644 index 0000000000..2f26997150 --- /dev/null +++ b/frontend/src/component/feature/strategy/AddStrategy/AddStrategyCard/AddStrategyCard.jsx @@ -0,0 +1,28 @@ +import { Typography, Card, Button, CardHeader, CardContent } from '@material-ui/core'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import { useStyles } from './AddStrategyCard.styles'; + +const CreateStrategyCard = ({ strategy, onClick }) => { + const styles = useStyles(); + + return ( + + + + {strategy.description} + + + + ); +}; + +CreateStrategyCard.propTypes = { + strategy: PropTypes.object.isRequired, + onClick: PropTypes.func.isRequired, +}; + +export default CreateStrategyCard; diff --git a/frontend/src/component/feature/strategy/AddStrategy/AddStrategyCard/AddStrategyCard.styles.js b/frontend/src/component/feature/strategy/AddStrategy/AddStrategyCard/AddStrategyCard.styles.js new file mode 100644 index 0000000000..1dd014dea5 --- /dev/null +++ b/frontend/src/component/feature/strategy/AddStrategy/AddStrategyCard/AddStrategyCard.styles.js @@ -0,0 +1,15 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + addStrategyCard: { + width: '200px', + minHeight: '200px', + textAlign: 'center', + borderTop: `4px solid ${theme.palette.primary.main}`, + display: 'flex', + flexDirection: 'column', + }, + addStrategyButton: { + marginTop: 'auto', + }, +})); diff --git a/frontend/src/component/feature/strategy/AddStrategy/utils.js b/frontend/src/component/feature/strategy/AddStrategy/utils.js new file mode 100644 index 0000000000..7092ba0f35 --- /dev/null +++ b/frontend/src/component/feature/strategy/AddStrategy/utils.js @@ -0,0 +1,13 @@ +export const resolveDefaultParamValue = (name, featureToggleName) => { + switch (name) { + case 'percentage': + case 'rollout': + return '100'; + case 'stickiness': + return 'default'; + case 'groupId': + return featureToggleName; + default: + return ''; + } +}; diff --git a/frontend/src/component/feature/strategy/EditStrategyModal/EditStrategyModal.jsx b/frontend/src/component/feature/strategy/EditStrategyModal/EditStrategyModal.jsx new file mode 100644 index 0000000000..139b2e9c13 --- /dev/null +++ b/frontend/src/component/feature/strategy/EditStrategyModal/EditStrategyModal.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Dialog, DialogContent, DialogTitle, DialogActions } from '@material-ui/core'; + +import FlexibleStrategy from './FlexibleStrategy'; +import DefaultStrategy from './default-strategy'; +import UserWithIdStrategy from './user-with-id-strategy'; +import GeneralStrategy from './general-strategy'; +import StrategyConstraints from '../StrategyConstraint/StrategyConstraintInput'; + +import { getHumanReadbleStrategyName } from '../../../../utils/strategy-names'; + +const EditStrategyModal = ({ onCancel, strategy, saveStrategy, updateStrategy, strategyDefinition }) => { + const updateParameters = parameters => { + const updatedStrategy = { ...strategy, parameters }; + updateStrategy(updatedStrategy); + }; + + const updateConstraints = constraints => { + const updatedStrategy = { ...strategy, constraints }; + updateStrategy(updatedStrategy); + }; + + const updateParameter = async (field, value) => { + const parameters = { ...strategy.parameters }; + parameters[field] = value; + updateParameters(parameters); + }; + + const resolveInputType = () => { + switch (strategyDefinition.name) { + case 'default': + return DefaultStrategy; + case 'flexibleRollout': + return FlexibleStrategy; + case 'userWithId': + return UserWithIdStrategy; + default: + return GeneralStrategy; + } + }; + + const Type = resolveInputType(); + + const { parameters } = strategy; + + return ( + + + Configure {getHumanReadbleStrategyName(strategy.name)} strategy + + +
    + +
    + +
    +
    + +
    + + + + +
    + ); +}; + +EditStrategyModal.propTypes = { + strategy: PropTypes.object.isRequired, + updateStrategy: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + saveStrategy: PropTypes.func.isRequired, + strategyDefinition: PropTypes.object.isRequired, + context: PropTypes.array, // TODO: fix me +}; + +export default EditStrategyModal; diff --git a/frontend/src/component/feature/strategy/EditStrategyModal/FlexibleStrategy.jsx b/frontend/src/component/feature/strategy/EditStrategyModal/FlexibleStrategy.jsx new file mode 100644 index 0000000000..95114c9e03 --- /dev/null +++ b/frontend/src/component/feature/strategy/EditStrategyModal/FlexibleStrategy.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import InputPercentage from './input-percentage'; +import Select from '../../../common/select'; +import { TextField, Typography } from '@material-ui/core'; + +const builtInStickinessOptions = [ + { key: 'default', label: 'default' }, + { key: 'userId', label: 'userId' }, + { key: 'sessionId', label: 'sessionId' }, + { key: 'random', label: 'random' }, +]; + +const FlexibleStrategy = ({ updateParameter, parameters, context = [] }) => { + const onUpdate = field => (_, newValue) => { + updateParameter(field, newValue); + }; + + const resolveStickiness = () => + builtInStickinessOptions.concat( + context + .filter(c => c.stickiness) + .filter(c => !builtInStickinessOptions.find(s => s.key === c.name)) + .map(c => ({ key: c.name, label: c.name })) + ); + + const stickinessOptions = resolveStickiness(); + + const rollout = parameters.rollout; + const stickiness = parameters.stickiness; + const groupId = parameters.groupId; + + return ( +
    + +
    +
    + + Stickiness + +
    + - ) : ( - updateConstraint(values, 'values')} - /> - )} - - - - - - ); - } -} diff --git a/frontend/src/component/feature/strategy/constraint/strategy-constraint-input.jsx b/frontend/src/component/feature/strategy/constraint/strategy-constraint-input.jsx deleted file mode 100644 index 47a2849345..0000000000 --- a/frontend/src/component/feature/strategy/constraint/strategy-constraint-input.jsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Tooltip, Icon } from 'react-mdl'; -import StrategyConstraintInputField from './strategy-constraint-input-field'; - -export default class StrategyConstraintInput extends Component { - static propTypes = { - constraints: PropTypes.array.isRequired, - updateConstraints: PropTypes.func.isRequired, - contextNames: PropTypes.array.isRequired, - contextFields: PropTypes.array.isRequired, - enabled: PropTypes.bool.isRequired, - }; - - constructor() { - super(); - this.state = { errors: [] }; - } - - addConstraint = evt => { - evt.preventDefault(); - const { constraints, updateConstraints, contextNames } = this.props; - - const updatedConstraints = [...constraints]; - updatedConstraints.push({ contextName: contextNames[0], operator: 'IN', values: [] }); - - updateConstraints(updatedConstraints); - }; - - removeConstraint = (index, evt) => { - evt.preventDefault(); - const { constraints, updateConstraints } = this.props; - - const updatedConstraints = [...constraints]; - updatedConstraints.splice(index, 1); - - updateConstraints(updatedConstraints); - }; - - updateConstraint = (index, value, field) => { - const { constraints } = this.props; - - // TOOD: value should be array - const updatedConstraints = [...constraints]; - const constraint = updatedConstraints[index]; - - constraint[field] = value; - - this.props.updateConstraints(updatedConstraints); - }; - - render() { - const { constraints, contextFields, enabled } = this.props; - - if (!enabled) { - return null; - } - - return ( -
    - {'Constraints '} - Use context fields to constrain the activation strategy.}> - - - - - {constraints.map((c, index) => ( - - ))} - -
    -

    - - Add constraint - -

    -
    - ); - } -} diff --git a/frontend/src/component/feature/strategy/flexible-rollout-strategy-container.jsx b/frontend/src/component/feature/strategy/flexible-rollout-strategy-container.jsx deleted file mode 100644 index d79142db50..0000000000 --- a/frontend/src/component/feature/strategy/flexible-rollout-strategy-container.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { connect } from 'react-redux'; -import FlexibleRolloutStrategy from './flexible-rollout-strategy'; - -const mapStateToProps = state => ({ - context: state.context.toJS(), -}); - -const FormAddContainer = connect(mapStateToProps, undefined)(FlexibleRolloutStrategy); - -export default FormAddContainer; diff --git a/frontend/src/component/feature/strategy/flexible-rollout-strategy.jsx b/frontend/src/component/feature/strategy/flexible-rollout-strategy.jsx deleted file mode 100644 index 8ed746bff0..0000000000 --- a/frontend/src/component/feature/strategy/flexible-rollout-strategy.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { Component } from 'react'; -import { Textfield } from 'react-mdl'; -import PropTypes from 'prop-types'; -import strategyInputProps from './strategy-input-props'; -import Select from '../../common/select'; - -import StrategyInputPercentage from './input-percentage'; - -const builtInStickinessOptions = [ - { key: 'default', label: 'default' }, - { key: 'userId', label: 'userId' }, - { key: 'sessionId', label: 'sessionId' }, - { key: 'random', label: 'random' }, -]; - -export default class FlexibleRolloutStrategy extends Component { - static propTypes = { ...strategyInputProps, context: PropTypes.array }; - - onUpdate = (field, evt) => { - evt.preventDefault(); - const value = evt.target.value; - this.props.updateParameter(field, value); - }; - - resolveStickiness = () => { - const { context } = this.props; - return builtInStickinessOptions.concat( - context - .filter(c => c.stickiness) - .filter(c => !builtInStickinessOptions.find(s => s.key === c.name)) - .map(c => ({ key: c.name, label: c.name })) - ); - }; - - render() { - const { editable, parameters, index } = this.props; - const stickinessOptions = this.resolveStickiness(); - - const rollout = parameters.rollout; - const stickiness = parameters.stickiness; - const groupId = parameters.groupId; - - return ( -
    -
    - Rollout - this.onUpdate('rollout', evt)} - id={`${index}-groupId`} - /> -
    - - - +
    +
    + default +
    + + + + +
    + + + Stickiness + + +
    +
       - + By overriding the stickiness you can control which parameter you want to be used in order to ensure consistent traffic allocation across variants. -

    +

    Variants allows you to return a variant object if the feature toggle is considered enabled for the current request. When using variants you should use the

    - - Add variant - + + Add variant + +
    `; @@ -263,7 +575,9 @@ exports[`renders correctly with without variants and no permissions 1`] = ` } } > -

    +

    Variants allows you to return a variant object if the feature toggle is considered enabled for the current request. When using variants you should use the 'OverrideConfig'); test('renders correctly with without variants', () => { diff --git a/frontend/src/component/feature/variant/add-variant.jsx b/frontend/src/component/feature/variant/add-variant.jsx index 12958851fe..bff31b851d 100644 --- a/frontend/src/component/feature/variant/add-variant.jsx +++ b/frontend/src/component/feature/variant/add-variant.jsx @@ -1,15 +1,12 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import Modal from 'react-modal'; -import { Button, Textfield, DialogActions, Grid, Cell, Icon, Switch } from 'react-mdl'; -import styles from './variant.module.scss'; +import { TextField, FormControl, FormControlLabel, Grid, Icon, Switch } from '@material-ui/core'; +import Dialog from '../../common/Dialogue'; import MySelect from '../../common/select'; import { trim, modalStyles } from '../../common/util'; import { weightTypes } from './enums'; import OverrideConfig from './e-override-config'; -Modal.setAppElement('#app'); - const payloadOptions = [ { key: 'string', label: 'string' }, { key: 'json', label: 'json' }, @@ -150,101 +147,105 @@ function AddVariant({ showDialog, closeDialog, save, validateName, editVariant, const isFixWeight = data.weightType === weightTypes.FIX; return ( - -

    {title}

    + + <> +

    {title}

    -
    -

    {error.general}

    - -
    - - - - % - - - - Custom percentage - - - -

    - Payload - -

    - - - - - - - - - {overrides.length > 0 && ( -

    - Overrides - + +

    {error.general}

    + +
    + + + + % + + + + + } + label="Custom percentage" + /> + + + +

    + Payload +

    - )} + + + + + + + + + {overrides.length > 0 && ( +

    + Overrides + +

    + )} - - - Add override - - - - - - - + + + Add override + + + +
    ); } diff --git a/frontend/src/component/feature/variant/e-override-config.jsx b/frontend/src/component/feature/variant/e-override-config.jsx index 3e751f25c3..76ec5e5323 100644 --- a/frontend/src/component/feature/variant/e-override-config.jsx +++ b/frontend/src/component/feature/variant/e-override-config.jsx @@ -2,14 +2,17 @@ import { connect } from 'react-redux'; import React from 'react'; import PropTypes from 'prop-types'; -import { Grid, Cell, IconButton } from 'react-mdl'; -import Select from 'react-select'; +import { Grid, IconButton, Icon } from '@material-ui/core'; import MySelect from '../../common/select'; import InputListField from '../../common/input-list-field'; import { selectStyles } from '../../common'; +import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; function OverrideConfig({ overrides, updateOverrideType, updateOverrideValues, removeOverride, contextDefinitions }) { - const contextNames = contextDefinitions.map(c => ({ key: c.name, label: c.name })); + const contextNames = contextDefinitions.map(c => ({ + key: c.name, + label: c.name, + })); const updateValues = i => values => { updateOverrideValues(i, values); @@ -26,8 +29,8 @@ function OverrideConfig({ overrides, updateOverrideType, updateOverrideValues, r const options = legalValues.map(v => ({ value: v, label: v, key: v })); return ( - - + + - - - {legalValues && legalValues.length > 0 ? ( -
    - + + + +
    + + + Project + + +
    +
    + + + + + +
    - - Disabled + + + + - + + + + Disabled +
    - - - Status - - - - - Set toggle Active - - - Mark toggle as Stale - - - - + + ACTIVE + + + arrow_drop_down + + + + + - Clone - + - - Archive - + + Archive + +
    - +

    - - - Activation - - - Metrics - - - V - - ariants - - - - L - - og - - - - +
    + + + + +
    + + + +
    +
    +
    + +
    +
      +
    • +
      + + extension + +
      +
      + + + + Another + + + +

      + another's description +

      +
      +
      - - - - - + > + +
      +
    • +
    +
    + `; diff --git a/frontend/src/component/strategies/__tests__/__snapshots__/strategy-details-component-test.jsx.snap b/frontend/src/component/strategies/__tests__/__snapshots__/strategy-details-component-test.jsx.snap index 2dd87b1e64..b3a1c79fbb 100644 --- a/frontend/src/component/strategies/__tests__/__snapshots__/strategy-details-component-test.jsx.snap +++ b/frontend/src/component/strategies/__tests__/__snapshots__/strategy-details-component-test.jsx.snap @@ -1,168 +1,330 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders correctly with one strategy 1`] = ` - -
    -
    Another -
    - - another's description - +
    - +
    +
    - - Details - - - Edit - - -
    +
    + another's description +
    - - - +
    -
    - Parameters -
    -
    - - - - customParam - - - ( - list - ) - - - - - - -
    - Applications using this strategy -
    -
    - - - - - - appA - - app description - - - - - -
    - -
    - Toggles using this strategy -
    -
    - - - - - toggleA - - - - -
    - + Details + + + + +
    + +
    +
    +
    +
    + +
    -
    + + `; exports[`renders correctly with one strategy without permissions 1`] = ` -
    -
    - Strategies -
    -
    - -
    - - - - - - Another - - - - - +
    - - + + + `; diff --git a/frontend/src/component/strategies/__tests__/list-component-test.jsx b/frontend/src/component/strategies/__tests__/list-component-test.jsx index aefbabd742..bd6366b3b6 100644 --- a/frontend/src/component/strategies/__tests__/list-component-test.jsx +++ b/frontend/src/component/strategies/__tests__/list-component-test.jsx @@ -1,11 +1,11 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { ThemeProvider } from '@material-ui/core'; -import StrategiesListComponent from '../list-component'; +import StrategiesListComponent from '../StrategiesList/StrategiesList'; import renderer from 'react-test-renderer'; import { CREATE_STRATEGY, DELETE_STRATEGY } from '../../../permissions'; - -jest.mock('react-mdl'); +import theme from '../../../themes/main-theme'; test('renders correctly with one strategy', () => { const strategy = { @@ -14,15 +14,17 @@ test('renders correctly with one strategy', () => { }; const tree = renderer.create( - [CREATE_STRATEGY, DELETE_STRATEGY].indexOf(permission) !== -1} - /> + + [CREATE_STRATEGY, DELETE_STRATEGY].indexOf(permission) !== -1} + /> + ); @@ -36,15 +38,17 @@ test('renders correctly with one strategy without permissions', () => { }; const tree = renderer.create( - false} - /> + + false} + /> + ); diff --git a/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx b/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx index 65c3beaba3..99cf0a5412 100644 --- a/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx +++ b/frontend/src/component/strategies/__tests__/strategy-details-component-test.jsx @@ -1,11 +1,10 @@ import React from 'react'; - +import { ThemeProvider } from '@material-ui/core'; import StrategyDetails from '../strategy-details-component'; import renderer from 'react-test-renderer'; import { UPDATE_STRATEGY } from '../../../permissions'; import { MemoryRouter } from 'react-router-dom'; - -jest.mock('react-mdl'); +import theme from '../../../themes/main-theme'; test('renders correctly with one strategy', () => { const strategy = { @@ -35,18 +34,20 @@ test('renders correctly with one strategy', () => { ]; const tree = renderer.create( - [UPDATE_STRATEGY].indexOf(permission) !== -1} - /> + + [UPDATE_STRATEGY].indexOf(permission) !== -1} + /> + ); diff --git a/frontend/src/component/strategies/form-container.js b/frontend/src/component/strategies/form-container.js index 4f13e21023..41b1513129 100644 --- a/frontend/src/component/strategies/form-container.js +++ b/frontend/src/component/strategies/form-container.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { createStrategy, updateStrategy } from '../../store/strategy/actions'; -import AddStrategy from './from-strategy'; +import AddStrategy from './form-strategy'; import { loadNameFromHash } from '../common/util'; class WrapperComponent extends Component { diff --git a/frontend/src/component/strategies/form-strategy.jsx b/frontend/src/component/strategies/form-strategy.jsx new file mode 100644 index 0000000000..fc15d87341 --- /dev/null +++ b/frontend/src/component/strategies/form-strategy.jsx @@ -0,0 +1,165 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { Typography, TextField, FormControlLabel, Checkbox, Button, Icon } from '@material-ui/core'; + +import MySelect from '../common/select'; +import PageContent from '../common/PageContent/PageContent'; +import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; + +import { styles as commonStyles, FormButtons } from '../common'; +import { trim } from '../common/util'; + +function gerArrayWithEntries(num) { + return Array.from(Array(num)); +} + +const paramTypesOptions = [ + { key: 'string', label: 'string' }, + { key: 'percentage', label: 'percentage' }, + { key: 'list', label: 'list' }, + { key: 'number', label: 'number' }, + { key: 'boolean', label: 'boolean' }, +]; + +const Parameter = ({ set, input = {}, index }) => { + const handleTypeChange = event => { + set({ type: event.target.value }); + }; + + return ( +
    + set({ name: target.value }, true)} + value={input.name || ''} + variant="outlined" + size="small" + /> + + + set({ description: target.value })} + value={input.description || ''} + variant="outlined" + size="small" + /> + set({ required: !input.required })} />} + label="Required" + /> +
    + ); +}; +Parameter.propTypes = { + input: PropTypes.object, + set: PropTypes.func, + index: PropTypes.number, +}; + +const Parameters = ({ input = [], count = 0, updateParameter }) => ( +
    + {gerArrayWithEntries(count).map((v, i) => ( + updateParameter(i, v, true)} index={i} input={input[i]} /> + ))} +
    +); + +Parameters.propTypes = { + input: PropTypes.array, + updateParameter: PropTypes.func.isRequired, + count: PropTypes.number, +}; + +class AddStrategy extends Component { + static propTypes = { + input: PropTypes.object, + setValue: PropTypes.func, + appParameter: PropTypes.func, + updateParameter: PropTypes.func, + clear: PropTypes.func, + onCancel: PropTypes.func, + onSubmit: PropTypes.func, + editMode: PropTypes.bool, + }; + + getHeaderTitle = () => { + const { editMode } = this.props; + if (editMode) return 'Edit strategy'; + return 'Create a new strategy'; + }; + + render() { + const { input, setValue, appParameter, onCancel, editMode = false, onSubmit, updateParameter } = this.props; + + return ( + + + Be careful! Changing a strategy definition might also require changes to the implementation + in the clients. + + } + /> + +
    + setValue('name', trim(target.value))} + value={input.name} + variant="outlined" + size="small" + /> + + setValue('description', target.value)} + value={input.description} + variant="outlined" + size="small" + /> + + + + + + +
    + ); + } +} + +export default AddStrategy; diff --git a/frontend/src/component/strategies/from-strategy.jsx b/frontend/src/component/strategies/from-strategy.jsx deleted file mode 100644 index 8acacdb349..0000000000 --- a/frontend/src/component/strategies/from-strategy.jsx +++ /dev/null @@ -1,164 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import { Textfield, IconButton, Menu, MenuItem, Checkbox, CardTitle, Card, CardActions } from 'react-mdl'; -import { styles as commonStyles, FormButtons } from '../common'; -import { trim } from '../common/util'; - -function gerArrayWithEntries(num) { - return Array.from(Array(num)); -} - -const Parameter = ({ set, input = {}, index }) => ( -
    - set({ name: target.value }, true)} - value={input.name} - /> -
    - - {input.type || 'string'} - evt.preventDefault()} /> - - - set({ type: 'string' })}>string - set({ type: 'percentage' })}>percentage - set({ type: 'list' })}>list - set({ type: 'number' })}>number - set({ type: 'boolean' })}>boolean - -
    - set({ description: target.value })} - value={input.description} - /> - set({ required: !input.required })} - ripple - /> -
    -); -Parameter.propTypes = { - input: PropTypes.object, - set: PropTypes.func, - index: PropTypes.number, -}; - -const EditHeader = () => ( -
    -

    Edit strategy

    -

    - Be carefull! Changing a strategy definition might also require changes to the implementation in the clients. -

    -
    -); - -const CreateHeader = () => ( -
    -

    Create a new Strategy definition

    -
    -); - -const Parameters = ({ input = [], count = 0, updateParameter }) => ( -
    - {gerArrayWithEntries(count).map((v, i) => ( - updateParameter(i, v, true)} index={i} input={input[i]} /> - ))} -
    -); - -Parameters.propTypes = { - input: PropTypes.array, - updateParameter: PropTypes.func.isRequired, - count: PropTypes.number, -}; - -class AddStrategy extends Component { - static propTypes = { - input: PropTypes.object, - setValue: PropTypes.func, - appParameter: PropTypes.func, - updateParameter: PropTypes.func, - clear: PropTypes.func, - onCancel: PropTypes.func, - onSubmit: PropTypes.func, - editMode: PropTypes.bool, - }; - - render() { - const { input, setValue, appParameter, onCancel, editMode = false, onSubmit, updateParameter } = this.props; - - return ( - - - {editMode ? : } - -
    -
    - setValue('name', trim(target.value))} - value={input.name} - /> -
    - setValue('description', target.value)} - value={input.description} - /> - - { - e.preventDefault(); - appParameter(); - }} - />{' '} -  Add parameter -
    - - - -
    -
    - ); - } -} - -export default AddStrategy; diff --git a/frontend/src/component/strategies/list-component.jsx b/frontend/src/component/strategies/list-component.jsx deleted file mode 100644 index c8d20eebc3..0000000000 --- a/frontend/src/component/strategies/list-component.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; - -import { List, ListItem, ListItemContent, IconButton, Card } from 'react-mdl'; -import { HeaderTitle, styles as commonStyles } from '../common'; -import { CREATE_STRATEGY, DELETE_STRATEGY } from '../../permissions'; - -import styles from './strategies.module.scss'; - -class StrategiesListComponent extends Component { - static propTypes = { - strategies: PropTypes.array.isRequired, - fetchStrategies: PropTypes.func.isRequired, - removeStrategy: PropTypes.func.isRequired, - deprecateStrategy: PropTypes.func.isRequired, - reactivateStrategy: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, - hasPermission: PropTypes.func.isRequired, - }; - - componentDidMount() { - this.props.fetchStrategies(); - } - - render() { - const { strategies, removeStrategy, hasPermission, reactivateStrategy, deprecateStrategy } = this.props; - - return ( - - this.props.history.push('/strategies/create')} - title="Add new strategy" - /> - ) : ( - '' - ) - } - /> - - {strategies.length > 0 ? ( - strategies.map((strategy, i) => ( - - - - {strategy.name} - {strategy.deprecated ? (Deprecated) : null} - - - - {strategy.deprecated ? ( - reactivateStrategy(strategy)} - /> - ) : ( - deprecateStrategy(strategy)} - /> - )} - {strategy.editable === false || !hasPermission(DELETE_STRATEGY) ? ( - {}} - /> - ) : ( - removeStrategy(strategy)} - /> - )} - - - )) - ) : ( - No entries - )} - - - ); - } -} - -export default StrategiesListComponent; diff --git a/frontend/src/component/strategies/show-strategy-component.js b/frontend/src/component/strategies/show-strategy-component.js index fac964b68f..dfa1885103 100644 --- a/frontend/src/component/strategies/show-strategy-component.js +++ b/frontend/src/component/strategies/show-strategy-component.js @@ -1,7 +1,10 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { Grid, Cell, List, ListItem, ListItemContent } from 'react-mdl'; -import { AppsLinkList, TogglesLinkList } from '../common'; +import { Grid, List, ListItem, ListItemText, ListItemAvatar, Icon, Tooltip } from '@material-ui/core'; +import { TogglesLinkList } from './toggles-link-list'; +import { AppsLinkList } from '../common'; +import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; +import styles from './strategies.module.scss'; class ShowStrategyComponent extends PureComponent { static propTypes = { @@ -13,10 +16,32 @@ class ShowStrategyComponent extends PureComponent { renderParameters(params) { if (params) { return params.map(({ name, type, description, required }, i) => ( - - - {name} ({type}) - + + + + add + + + } + elseShow={ + + + radio_button_unchecked + + + } + /> + + {name} ({type}) + + } + secondary={description} + /> )); } else { @@ -30,32 +55,33 @@ class ShowStrategyComponent extends PureComponent { const { parameters = [] } = strategy; return ( -
    - - {strategy.deprecated ? ( - -
    Deprecated
    -
    - ) : ( - '' - )} - +
    + + +
    Deprecated
    +
    + } + /> +
    Parameters

    {this.renderParameters(parameters)} - +
    - +
    Applications using this strategy

    -
    + - +
    Toggles using this strategy

    -
    +
    ); diff --git a/frontend/src/component/strategies/strategies.module.scss b/frontend/src/component/strategies/strategies.module.scss index 3ab4ad4f05..00e0cb9aca 100644 --- a/frontend/src/component/strategies/strategies.module.scss +++ b/frontend/src/component/strategies/strategies.module.scss @@ -11,5 +11,51 @@ a { color: #1d1818; } - -} \ No newline at end of file +} + +.formButtons { + padding-top: 1rem; +} + +.header { + padding: var(--card-header-padding); + margin-bottom: var(--card-margin-y); + word-break: break-all; + border-bottom: var(--default-border); + display: flex; + align-items: center; + justify-content: space-between; +} + +.header h1 { + font-size: var(--h1-size); +} + +.formContainer { + margin-bottom: 1.5rem; + max-width: 350px; +} + +.formContainer > *, +.inset > * { + margin: 0.5rem 0; +} + +.parameter_menu { + border-radius: 2px; + cursor: pointer; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.04), + 0 3px 1px -2px rgba(0, 0, 0, 0.1), 0 1px 5px 0 rgba(0, 0, 0, 0.12); + margin-left: 10px; + border: 1px solid #f1f1f1; + background-color: white; + padding: 10px 2px 10px 20px; +} + +.listcontainer { + padding: 0 0.5rem; +} + +.listItem { + padding: 0; +} diff --git a/frontend/src/component/strategies/strategy-details-component.jsx b/frontend/src/component/strategies/strategy-details-component.jsx index 7aee5d48f6..42046c79cd 100644 --- a/frontend/src/component/strategies/strategy-details-component.jsx +++ b/frontend/src/component/strategies/strategy-details-component.jsx @@ -1,15 +1,12 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { Tabs, Tab, ProgressBar, Grid, Cell } from 'react-mdl'; +import { Grid, Typography } from '@material-ui/core'; import ShowStrategy from './show-strategy-component'; import EditStrategy from './form-container'; -import { HeaderTitle } from '../common'; import { UPDATE_STRATEGY } from '../../permissions'; - -const TABS = { - view: 0, - edit: 1, -}; +import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; +import TabNav from '../common/TabNav/TabNav'; +import PageContent from '../common/PageContent/PageContent'; export default class StrategyDetails extends Component { static propTypes = { @@ -37,51 +34,51 @@ export default class StrategyDetails extends Component { } } - getTabContent(activeTabId) { - if (activeTabId === TABS.edit) { - return ; - } else { - return ( - - ); - } - } - - goToTab(tabName) { - this.props.history.push(`/strategies/${tabName}/${this.props.strategyName}`); - } - render() { - const activeTabId = TABS[this.props.activeTab] ? TABS[this.props.activeTab] : TABS.strategies; const strategy = this.props.strategy; - if (!strategy) { - return ; - } - - const tabContent = this.getTabContent(activeTabId); - + const tabData = [ + { + label: 'Details', + component: ( + + ), + }, + { + label: 'Edit', + component: , + }, + ]; return ( - - - - {strategy.editable === false || !this.props.hasPermission(UPDATE_STRATEGY) ? ( - '' - ) : ( - - this.goToTab('view')}>Details - this.goToTab('edit')}>Edit - - )} - -
    -
    {tabContent}
    -
    -
    -
    + + + + {strategy.description} + + +
    + } + elseShow={ +
    +
    + +
    +
    + } + /> + + +
    ); } } diff --git a/frontend/src/component/strategies/toggles-link-list.jsx b/frontend/src/component/strategies/toggles-link-list.jsx new file mode 100644 index 0000000000..7ff0ce5fd5 --- /dev/null +++ b/frontend/src/component/strategies/toggles-link-list.jsx @@ -0,0 +1,34 @@ +import { Icon, List, ListItem, ListItemAvatar, ListItemText, Tooltip } from '@material-ui/core'; +import styles from '../common/common.module.scss'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import React from 'react'; +import ConditionallyRender from '../common/ConditionallyRender/ConditionallyRender'; + +export const TogglesLinkList = ({ toggles }) => ( + + 0} + show={toggles.map(({ name, description = '-', enabled, icon = enabled ? 'play_arrow' : 'pause' }) => ( + + + + {icon} + + + + {name} + + } + secondary={description} + /> + + ))} + /> + +); +TogglesLinkList.propTypes = { + toggles: PropTypes.array, +}; diff --git a/frontend/src/component/styles.module.scss b/frontend/src/component/styles.module.scss index 57e04f0dfd..dd1a83488a 100644 --- a/frontend/src/component/styles.module.scss +++ b/frontend/src/component/styles.module.scss @@ -1,17 +1,29 @@ .container { + height: 100%; ul { margin: 0; } hr { margin: 0; height: auto; - border-color: rgba(0,0,0,.12); + border-color: rgba(0, 0, 0, 0.12); } } +.primaryBreadbrumb { + color: #fff; +} + +.headerTitleLink { + color: #fff; +} + .contentWrapper { - display: flex; - flex-direction: column; + margin: 0 auto; + flex: 1; + min-height: 100%; + width: 100%; + background-color: #ecebeb; } .content { @@ -20,7 +32,11 @@ margin-right: auto; margin-top: 16px; margin-bottom: 16px; - flex: 1 0 0%; +} + +.contentContainer { + padding: var(--page-padding); + height: 100%; } @media (max-width: 1260px) { @@ -78,6 +94,7 @@ .navigationLink { padding: 12px 20px !important; border-radius: 0 50px 50px 0; + text-decoration: none; } @media screen and (max-width: 1024px) { .navigationLink { diff --git a/frontend/src/component/tag-types/TagType.module.scss b/frontend/src/component/tag-types/TagType.module.scss new file mode 100644 index 0000000000..055a7bf575 --- /dev/null +++ b/frontend/src/component/tag-types/TagType.module.scss @@ -0,0 +1,41 @@ +.select { + min-width: 100px; +} + +.textfield { + margin-left: 15px; +} + +.header { + padding: var(--card-header-padding); + word-break: break-all; + border-bottom: var(--default-border); + display: flex; + align-items: center; + justify-content: space-between; +} + +.header h1 { + font-size: var(--h1-size); +} + +.container { + padding: var(--card-padding); +} + +.formButtons { + padding-top: 1rem; +} + +.tagListItem { + padding: 0; +} + +.tagTypeContainer { + max-width: 350px; +} + +.addTagTypeForm { + display: flex; + flex-direction: column; +} diff --git a/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx b/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx new file mode 100644 index 0000000000..3057eda650 --- /dev/null +++ b/frontend/src/component/tag-types/TagTypeList/TagTypeList.jsx @@ -0,0 +1,132 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { Link, useHistory } from 'react-router-dom'; + +import { + List, + ListItem, + ListItemIcon, + Icon, + ListItemText, + IconButton, + Button, + Tooltip, + Typography, +} from '@material-ui/core'; +import HeaderTitle from '../../common/HeaderTitle'; +import PageContent from '../../common/PageContent/PageContent'; +import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; +import { CREATE_TAG_TYPE, DELETE_TAG_TYPE } from '../../../permissions'; +import Dialogue from '../../common/Dialogue/Dialogue'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; + +import styles from '../TagType.module.scss'; + +const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType, hasPermission }) => { + const [deletion, setDeletion] = useState({ open: false }); + const history = useHistory(); + const smallScreen = useMediaQuery('(max-width:700px)'); + + useEffect(() => { + fetchTagTypes(); + }, []); + + let header = ( + + history.push('/tag-types/create')} + > + add + + + } + elseShow={ + + } + /> + } + /> + } + /> + ); + + const renderTagType = tagType => { + let link = ( + + {tagType.name} + + ); + let deleteButton = ( + + + setDeletion({ + open: true, + name: tagType.name, + }) + } + > + delete + + + ); + return ( + + + label + + + + + ); + }; + return ( + + + 0} + show={tagTypes.map(tagType => renderTagType(tagType))} + elseShow={No entries} + /> + + { + removeTagType(deletion.name); + setDeletion({ open: false }); + }} + onClose={() => { + setDeletion({ open: false }); + }} + > + Are you sure? + + + ); +}; + +TagTypeList.propTypes = { + tagTypes: PropTypes.array.isRequired, + fetchTagTypes: PropTypes.func.isRequired, + removeTagType: PropTypes.func.isRequired, + hasPermission: PropTypes.func.isRequired, +}; + +export default TagTypeList; diff --git a/frontend/src/component/tag-types/TagTypeList/index.jsx b/frontend/src/component/tag-types/TagTypeList/index.jsx new file mode 100644 index 0000000000..20507a5dfc --- /dev/null +++ b/frontend/src/component/tag-types/TagTypeList/index.jsx @@ -0,0 +1,3 @@ +import TagTypeList from './TagTypeList'; + +export default TagTypeList; diff --git a/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-create-component-test.js.snap b/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-create-component-test.js.snap index ead2668dad..bb27aafbde 100644 --- a/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-create-component-test.js.snap +++ b/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-create-component-test.js.snap @@ -1,191 +1,219 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`it supports editMode 1`] = ` - - - Update - Tag type - - - Tag types allows you to group tags together in the management UI - -
    +
    +

    + Update Tag type +

    +
    + + +
    -

    - - +

    + Tag types allows you to group tags together in the management UI +
    + +
    +
    + +   + +
    +
    +
    - -
    - - -     - Update - -   - - Cancel - -
    -
    - - +
    + `; exports[`renders correctly for creating 1`] = ` - - - Create - Tag type - - - Tag types allows you to group tags together in the management UI - -
    +
    +

    + Create Tag type +

    +
    + + +
    -

    - - +

    + Tag types allows you to group tags together in the management UI +
    + +
    +
    + +   + +
    +
    +
    - -
    - - -     - Create - -   - - Cancel - -
    -
    - - +
    + `; diff --git a/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-list-component-test.js.snap b/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-list-component-test.js.snap index 77fc364546..daeb91e665 100644 --- a/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-list-component-test.js.snap +++ b/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-list-component-test.js.snap @@ -1,141 +1,201 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders a list with elements correctly 1`] = ` -
    -
    - Tag Types -
    -
    -
    - +

    + Tag Types +

    +
    +
    + +
    - - + + + `; exports[`renders an empty list correctly 1`] = ` -
    -
    - Tag Types -
    -
    -
    - +

    + Tag Types +

    +
    +
    + +
    - - - No entries - - -
    +
    +
      +
    • + No entries +
    • +
    +
    + `; diff --git a/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js b/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js index 8c37b919ef..8a314b7249 100644 --- a/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js +++ b/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js @@ -1,23 +1,26 @@ import React from 'react'; - +import { ThemeProvider } from '@material-ui/core'; import TagTypes from '../form-tag-type-component'; import renderer from 'react-test-renderer'; +import theme from '../../../themes/main-theme'; -jest.mock('react-mdl'); +jest.mock('@material-ui/core/TextField'); test('renders correctly for creating', () => { const tree = renderer .create( - Promise.resolve(true)} - hasPermission={() => true} - tagType={{ name: '', description: '', icon: '' }} - editMode={false} - submit={jest.fn()} - /> + + Promise.resolve(true)} + hasPermission={() => true} + tagType={{ name: '', description: '', icon: '' }} + editMode={false} + submit={jest.fn()} + /> + ) .toJSON(); expect(tree).toMatchSnapshot(); @@ -26,16 +29,18 @@ test('renders correctly for creating', () => { test('it supports editMode', () => { const tree = renderer .create( - Promise.resolve(true)} - hasPermission={() => true} - tagType={{ name: '', description: '', icon: '' }} - editMode - submit={jest.fn()} - /> + + Promise.resolve(true)} + hasPermission={() => true} + tagType={{ name: '', description: '', icon: '' }} + editMode + submit={jest.fn()} + /> + ) .toJSON(); expect(tree).toMatchSnapshot(); diff --git a/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js b/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js index 2a82a002a9..0de6050d93 100644 --- a/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js +++ b/frontend/src/component/tag-types/__tests__/tag-type-list-component-test.js @@ -1,35 +1,47 @@ import React from 'react'; -import TagTypesList from '../list-component'; +import TagTypesList from '../TagTypeList'; import renderer from 'react-test-renderer'; import { MemoryRouter } from 'react-router-dom'; - -jest.mock('react-mdl'); +import { ThemeProvider } from '@material-ui/styles'; +import theme from '../../../themes/main-theme'; test('renders an empty list correctly', () => { const tree = renderer.create( - true} - /> + + + true} + /> + + ); expect(tree).toMatchSnapshot(); }); test('renders a list with elements correctly', () => { const tree = renderer.create( - - true} - /> - + + + true} + /> + + ); expect(tree).toMatchSnapshot(); }); diff --git a/frontend/src/component/tag-types/edit-tag-type-container.js b/frontend/src/component/tag-types/edit-tag-type-container.js index 431953688b..4372dc799b 100644 --- a/frontend/src/component/tag-types/edit-tag-type-container.js +++ b/frontend/src/component/tag-types/edit-tag-type-container.js @@ -15,7 +15,9 @@ const mapStateToProps = (state, props) => { const mapDispatchToProps = dispatch => ({ validateName: () => {}, - submit: tagType => updateTagType(tagType)(dispatch), + submit: tagType => { + updateTagType(tagType)(dispatch); + }, }); const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/frontend/src/component/tag-types/form-tag-type-component.js b/frontend/src/component/tag-types/form-tag-type-component.js index 95ff946a7a..5f0838d5e5 100644 --- a/frontend/src/component/tag-types/form-tag-type-component.js +++ b/frontend/src/component/tag-types/form-tag-type-component.js @@ -1,112 +1,93 @@ -import React, { Component } from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Textfield, Card, CardTitle, CardText, CardActions } from 'react-mdl'; -import { FormButtons, styles as commonStyles } from '../common'; -import { trim } from '../common/util'; -class AddTagTypeComponent extends Component { - constructor(props) { - super(props); - this.state = { - tagType: props.tagType, - errors: {}, - dirty: false, - currentLegalValue: '', - }; - } +import classnames from 'classnames'; - static getDerivedStateFromProps(props, state) { - if (!state.tagType && props.tagType.name) { - return { tagType: props.tagType }; - } else { - return null; - } - } +import { FormButtons } from '../common'; +import PageContent from '../common/PageContent/PageContent'; +import { Typography, TextField } from '@material-ui/core'; - onValidateName = async evt => { +import styles from './TagType.module.scss'; +import commonStyles from '../common/common.module.scss'; + +const AddTagTypeComponent = ({ tagType, validateName, submit, history, editMode }) => { + const [tagTypeName, setTagTypeName] = useState(tagType.name || ''); + const [tagTypeDescription, setTagTypeDescription] = useState(tagType.description || ''); + const [errors, setErrors] = useState({ + general: undefined, + name: undefined, + description: undefined, + }); + + const onValidateName = async evt => { evt.preventDefault(); const name = evt.target.value; - const { errors } = this.state; - const { validateName } = this.props; try { await validateName(name); - errors.name = undefined; + setErrors({ name: undefined }); } catch (err) { - errors.name = err.message; + setErrors({ name: err.message }); } - this.setState({ errors }); }; - setValue = (field, value) => { - const { tagType } = this.state; - tagType[field] = trim(value); - this.setState({ tagType, dirty: true }); - }; - - onCancel = evt => { + const onCancel = evt => { evt.preventDefault(); - this.props.history.push('/tag-types'); + history.push('/tag-types'); }; - onSubmit = async evt => { + const onSubmit = async evt => { evt.preventDefault(); - const { tagType } = this.state; - try { - await this.props.submit(tagType); - this.props.history.push('/tag-types'); - } catch (e) { - this.setState({ - errors: { - general: e.message, - }, + await submit({ + name: tagTypeName, + description: tagTypeDescription, }); + history.push('/tag-types'); + } catch (e) { + setErrors({ general: e.message }); } }; - - render() { - const { tagType, errors } = this.state; - const { editMode } = this.props; - const submitText = editMode ? 'Update' : 'Create'; - return ( - - - {submitText} Tag type - - Tag types allows you to group tags together in the management UI -
    -
    -

    {errors.general}

    - this.setValue('name', v.target.value)} - /> - this.setValue('description', v.target.value)} - /> -
    - - - + const submitText = editMode ? 'Update' : 'Create'; + return ( + +
    + + Tag types allows you to group tags together in the management UI + + + setTagTypeName(v.target.value.trim())} + variant="outlined" + size="small" + /> + setTagTypeDescription(v.target.value)} + variant="outlined" + size="small" + /> +
    + +
    - - ); - } -} +
    +
    + ); +}; AddTagTypeComponent.propTypes = { tagType: PropTypes.object.isRequired, diff --git a/frontend/src/component/tag-types/list-container.jsx b/frontend/src/component/tag-types/index.jsx similarity index 72% rename from frontend/src/component/tag-types/list-container.jsx rename to frontend/src/component/tag-types/index.jsx index 3ebe6092a2..8701bf5f71 100644 --- a/frontend/src/component/tag-types/list-container.jsx +++ b/frontend/src/component/tag-types/index.jsx @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import TagTypesListComponent from './list-component.jsx'; +import TagTypesListComponent from './TagTypeList'; import { fetchTagTypes, removeTagType } from '../../store/tag-type/actions'; import { hasPermission } from '../../permissions'; @@ -13,10 +13,7 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => ({ removeTagType: tagtype => { - // eslint-disable-next-line no-alert - if (window.confirm('Are you sure you want to remove this tag type?')) { - removeTagType(tagtype)(dispatch); - } + removeTagType(tagtype)(dispatch); }, fetchTagTypes: () => fetchTagTypes()(dispatch), }); diff --git a/frontend/src/component/tag-types/list-component.jsx b/frontend/src/component/tag-types/list-component.jsx deleted file mode 100644 index a49117e419..0000000000 --- a/frontend/src/component/tag-types/list-component.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; - -import { List, ListItem, ListItemContent, Card, IconButton } from 'react-mdl'; -import { HeaderTitle, styles as commonStyles } from '../common'; -import { CREATE_TAG_TYPE, DELETE_TAG_TYPE } from '../../permissions'; - -class TagTypesListComponent extends Component { - static propTypes = { - tagTypes: PropTypes.array.isRequired, - fetchTagTypes: PropTypes.func.isRequired, - removeTagType: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, - hasPermission: PropTypes.func.isRequired, - }; - - componentDidMount() { - this.props.fetchTagTypes(); - } - - removeTagType = (tagType, evt) => { - evt.preventDefault(); - this.props.removeTagType(tagType); - }; - - render() { - const { tagTypes, hasPermission } = this.props; - return ( - - this.props.history.push('/tag-types/create')} - title="Add new tag type" - /> - ) : ( - '' - ) - } - /> - - {tagTypes.length > 0 ? ( - tagTypes.map((tagType, i) => ( - - - - {tagType.name} - - - {hasPermission(DELETE_TAG_TYPE) ? ( - - ) : ( - '' - )} - - )) - ) : ( - No entries - )} - - - ); - } -} - -export default TagTypesListComponent; diff --git a/frontend/src/component/tags/Tag.module.scss b/frontend/src/component/tags/Tag.module.scss new file mode 100644 index 0000000000..b2a967e5a3 --- /dev/null +++ b/frontend/src/component/tags/Tag.module.scss @@ -0,0 +1,32 @@ +.select { + min-width: 100px; +} + +.textfield { + margin-left: 15px; +} + +.header { + padding: var(--card-header-padding); + word-break: break-all; + border-bottom: var(--default-border); + display: flex; + align-items: center; + justify-content: space-between; +} + +.header h1 { + font-size: var(--h1-size); +} + +.container { + padding: var(--card-padding); +} + +.formbuttons { + padding-top: 1rem; +} + +.tagListItem { + padding: 0; +} diff --git a/frontend/src/component/tags/TagList/TagList.jsx b/frontend/src/component/tags/TagList/TagList.jsx new file mode 100644 index 0000000000..7afa9b7380 --- /dev/null +++ b/frontend/src/component/tags/TagList/TagList.jsx @@ -0,0 +1,105 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import { useHistory } from 'react-router-dom'; + +import { Button, Icon, IconButton, List, ListItem, ListItemIcon, ListItemText, Tooltip } from '@material-ui/core'; +import { CREATE_TAG, DELETE_TAG } from '../../../permissions'; +import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; +import HeaderTitle from '../../common/HeaderTitle'; +import PageContent from '../../common/PageContent/PageContent'; + +import { useStyles } from './TagList.styles'; + +const TagList = ({ tags, fetchTags, removeTag, hasPermission }) => { + const history = useHistory(); + const smallScreen = useMediaQuery('(max-width:700px)'); + const styles = useStyles(); + + useEffect(() => { + fetchTags(); + }, []); + + const remove = (tag, evt) => { + evt.preventDefault(); + removeTag(tag); + }; + + const listItem = tag => ( + + + label + + + } + /> + + ); + + const DeleteButton = ({ tagType, tagValue }) => ( + + remove({ type: tagType, value: tagValue }, e)}> + delete + + + ); + + DeleteButton.propTypes = { + tagType: PropTypes.string, + tagValue: PropTypes.string, + }; + + const AddButton = ({ hasPermission }) => ( + history.push('/tags/create')}> + add + + } + elseShow={ + + + + } + /> + } + /> + ); + return ( + } />}> + + 0} + show={tags.map(tag => listItem(tag))} + elseShow={ + + + + } + /> + + + ); +}; + +TagList.propTypes = { + tags: PropTypes.array.isRequired, + fetchTags: PropTypes.func.isRequired, + removeTag: PropTypes.func.isRequired, + hasPermission: PropTypes.func.isRequired, +}; + +export default TagList; diff --git a/frontend/src/component/tags/TagList/TagList.styles.js b/frontend/src/component/tags/TagList/TagList.styles.js new file mode 100644 index 0000000000..05e9821937 --- /dev/null +++ b/frontend/src/component/tags/TagList/TagList.styles.js @@ -0,0 +1,7 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles({ + tagListItem: { + padding: 0, + }, +}); diff --git a/frontend/src/component/tags/TagList/index.jsx b/frontend/src/component/tags/TagList/index.jsx new file mode 100644 index 0000000000..bcd0059830 --- /dev/null +++ b/frontend/src/component/tags/TagList/index.jsx @@ -0,0 +1,3 @@ +import TagsListComponent from './TagList'; + +export default TagsListComponent; diff --git a/frontend/src/component/tags/form-tag-component.js b/frontend/src/component/tags/form-tag-component.js index 93f58eb8c3..3c7dfd6d2b 100644 --- a/frontend/src/component/tags/form-tag-component.js +++ b/frontend/src/component/tags/form-tag-component.js @@ -1,9 +1,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { Textfield, Card, CardTitle, CardActions } from 'react-mdl'; -import { FormButtons, styles as commonStyles } from '../common'; +import { TextField } from '@material-ui/core'; +import styles from './Tag.module.scss'; +import { FormButtons } from '../common'; import TagTypeSelect from '../feature/tag-type-select-container'; -import { trim } from '../common/util'; +import PageContent from '../common/PageContent/PageContent'; + class AddTagComponent extends Component { constructor(props) { super(props); @@ -56,33 +58,35 @@ class AddTagComponent extends Component { const { tag, errors } = this.state; const submitText = 'Create'; return ( - - - {submitText} Tag - -
    -
    + +
    +

    {errors.general}

    this.setValue('type', v.target.value)} + className={styles.select} /> - this.setValue('value', trim(v.target.value))} + error={errors.value !== undefined} + helperText={errors.value} + onChange={v => this.setValue('value', v.target.value)} + className={styles.textfield} /> -
    - - - - - +
    + +
    + +
    + ); } } diff --git a/frontend/src/component/tags/list-container.jsx b/frontend/src/component/tags/index.jsx similarity index 93% rename from frontend/src/component/tags/list-container.jsx rename to frontend/src/component/tags/index.jsx index 221c003e4c..c6b529c88d 100644 --- a/frontend/src/component/tags/list-container.jsx +++ b/frontend/src/component/tags/index.jsx @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import TagsListComponent from './list-component.jsx'; +import TagsListComponent from './TagList'; import { fetchTags, removeTag } from '../../store/tag/actions'; import { hasPermission } from '../../permissions'; diff --git a/frontend/src/component/tags/list-component.jsx b/frontend/src/component/tags/list-component.jsx deleted file mode 100644 index a8c5dfbe56..0000000000 --- a/frontend/src/component/tags/list-component.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import { List, ListItem, ListItemContent, Card, IconButton } from 'react-mdl'; -import { HeaderTitle, styles as commonStyles } from '../common'; -import { CREATE_TAG, DELETE_TAG } from '../../permissions'; - -class TagsListComponent extends Component { - static propTypes = { - tags: PropTypes.array.isRequired, - fetchTags: PropTypes.func.isRequired, - removeTag: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, - hasPermission: PropTypes.func.isRequired, - }; - - componentDidMount() { - this.props.fetchTags(); - } - - removeTag = (tag, evt) => { - evt.preventDefault(); - this.props.removeTag(tag); - }; - - render() { - const { tags, hasPermission } = this.props; - return ( - - this.props.history.push('/tags/create')} - title="Add new tag" - /> - ) : ( - '' - ) - } - /> - - {tags.length > 0 ? ( - tags.map((tag, i) => ( - - - {tag.value} - - {hasPermission(DELETE_TAG) ? ( - - ) : ( - '' - )} - - )) - ) : ( - No entries - )} - - - ); - } -} - -export default TagsListComponent; diff --git a/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx b/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx new file mode 100644 index 0000000000..d6975d6637 --- /dev/null +++ b/frontend/src/component/user/PasswordAuth/PasswordAuth.jsx @@ -0,0 +1,150 @@ +import React, { useState } from 'react'; +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import { CardActions, Button, TextField, Typography } from '@material-ui/core'; +import ConditionallyRender from '../../common/ConditionallyRender'; +import { useHistory } from 'react-router'; +import { useCommonStyles } from '../../../common.styles'; +import { useStyles } from './PasswordAuth.styles'; + +const PasswordAuth = ({ authDetails, passwordLogin, loadInitialData }) => { + const commonStyles = useCommonStyles(); + const styles = useStyles(); + const history = useHistory(); + const [showFields, setShowFields] = useState(false); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [errors, setErrors] = useState({ + usernameError: '', + passwordError: '', + }); + + const onShowOptions = e => { + e.preventDefault(); + setShowFields(true); + }; + + const handleSubmit = async evt => { + evt.preventDefault(); + + if (!username) { + setErrors(prev => ({ + ...prev, + usernameError: 'This is a required field', + })); + } + if (!password) { + setErrors(prev => ({ + ...prev, + passwordError: 'This is a required field', + })); + } + + if (!password || !username) { + return; + } + + const user = { username, password }; + const path = evt.target.action; + + try { + await passwordLogin(path, user); + await loadInitialData(); + history.push(`/`); + } catch (error) { + if (error.statusCode === 404 || error.statusCode === 400) { + setErrors(prev => ({ + ...prev, + apiError: 'Invalid login details', + })); + setPassword(''); + setUsername(''); + } else { + setErrors({ + apiError: 'Unknown error while trying to authenticate.', + }); + } + } + }; + + const renderLoginForm = () => { + const { usernameError, passwordError, apiError } = errors; + + return ( +
    + {authDetails.message} + + {apiError} + +
    + setUsername(evt.target.value)} + value={username} + error={!!usernameError} + helperText={usernameError} + variant="outlined" + size="small" + /> + setPassword(evt.target.value)} + name="password" + type="password" + value={password} + error={!!passwordError} + helperText={passwordError} + variant="outlined" + size="small" + /> + + +
    +
    + ); + }; + + const renderWithOptions = options => ( +
    + {options.map(o => ( + + + + ))} + + Show more options + + } + /> +
    + ); + + const { options = [] } = authDetails; + + return ( + 0} + show={renderWithOptions(options)} + elseShow={renderLoginForm()} + /> + ); +}; + +PasswordAuth.propTypes = { + authDetails: PropTypes.object.isRequired, + passwordLogin: PropTypes.func.isRequired, + loadInitialData: PropTypes.func.isRequired, + history: PropTypes.object.isRequired, +}; + +export default PasswordAuth; diff --git a/frontend/src/component/user/PasswordAuth/PasswordAuth.styles.js b/frontend/src/component/user/PasswordAuth/PasswordAuth.styles.js new file mode 100644 index 0000000000..1d68bc5be6 --- /dev/null +++ b/frontend/src/component/user/PasswordAuth/PasswordAuth.styles.js @@ -0,0 +1,18 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles(theme => ({ + loginContainer: { + minWidth: '350px', + [theme.breakpoints.down('xs')]: { + width: '100%', + minWidth: 'auto', + }, + }, + contentContainer: { + display: 'flex', + flexDirection: 'column', + }, + apiError: { + color: theme.palette.error.main, + }, +})); diff --git a/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx b/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx new file mode 100644 index 0000000000..f790c42ba7 --- /dev/null +++ b/frontend/src/component/user/SimpleAuth/SimpleAuth.jsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Button, TextField } from '@material-ui/core'; + +import styles from './SimpleAuth.module.scss'; + +const SimpleAuth = ({ insecureLogin, loadInitialData, history, authDetails }) => { + const [email, setEmail] = useState(''); + + const handleSubmit = evt => { + evt.preventDefault(); + const user = { email }; + const path = evt.target.action; + + insecureLogin(path, user) + .then(loadInitialData) + .then(() => history.push(`/`)); + }; + + const handleChange = e => { + const value = e.target.value; + setEmail(value); + }; + + return ( +
    +
    +

    {authDetails.message}

    +

    + This instance of Unleash is not set up with a secure authentication provider. You can read more + about{' '} + + securing Unleash on GitHub + +

    + +
    + +
    + +
    +
    +
    + ); +}; + +SimpleAuth.propTypes = { + authDetails: PropTypes.object.isRequired, + insecureLogin: PropTypes.func.isRequired, + loadInitialData: PropTypes.func.isRequired, + history: PropTypes.object.isRequired, +}; + +export default SimpleAuth; diff --git a/frontend/src/component/user/SimpleAuth/SimpleAuth.module.scss b/frontend/src/component/user/SimpleAuth/SimpleAuth.module.scss new file mode 100644 index 0000000000..131625e4e6 --- /dev/null +++ b/frontend/src/component/user/SimpleAuth/SimpleAuth.module.scss @@ -0,0 +1,3 @@ +.container > * { + margin: 1rem 0; +} \ No newline at end of file diff --git a/frontend/src/component/user/SimpleAuth/index.jsx b/frontend/src/component/user/SimpleAuth/index.jsx new file mode 100644 index 0000000000..5d169efa90 --- /dev/null +++ b/frontend/src/component/user/SimpleAuth/index.jsx @@ -0,0 +1,3 @@ +import SimpleAuth from './SimpleAuth'; + +export default SimpleAuth; diff --git a/frontend/src/component/user/authentication-component.jsx b/frontend/src/component/user/authentication-component.jsx index 8a1f7d7b95..ae2a394fa5 100644 --- a/frontend/src/component/user/authentication-component.jsx +++ b/frontend/src/component/user/authentication-component.jsx @@ -1,36 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Card, CardTitle, CardText } from 'react-mdl'; -import Modal from 'react-modal'; -import AuthenticationSimpleComponent from './authentication-simple-component'; +import { Dialog, Icon, DialogTitle } from '@material-ui/core'; +import SimpleAuth from './SimpleAuth/SimpleAuth'; import AuthenticationCustomComponent from './authentication-custom-component'; -import AuthenticationPasswordComponent from './authentication-password-component'; +import AuthenticationPasswordComponent from './PasswordAuth/PasswordAuth'; const SIMPLE_TYPE = 'unsecure'; const PASSWORD_TYPE = 'password'; -const customStyles = { - overlay: { - position: 'fixed', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: 'rgba(0, 0, 0, 0.75)', - zIndex: 99999, - }, - content: { - top: '50%', - left: '50%', - right: 'auto', - bottom: 'auto', - marginRight: '-50%', - transform: 'translate(-50%, -50%)', - backgroundColor: 'transparent', - padding: 0, - overflow: 'none', - }, -}; +const customStyles = {}; class AuthComponent extends React.Component { static propTypes = { @@ -57,7 +35,7 @@ class AuthComponent extends React.Component { ); } else if (authDetails.type === SIMPLE_TYPE) { content = ( - - - - - Action Required - - {content} - - + + + + person Login + + + +
    {content}
    +
    ); } diff --git a/frontend/src/component/user/authentication-custom-component.jsx b/frontend/src/component/user/authentication-custom-component.jsx index dd051df39d..7c0467838f 100644 --- a/frontend/src/component/user/authentication-custom-component.jsx +++ b/frontend/src/component/user/authentication-custom-component.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { CardActions, Button } from 'react-mdl'; +import { CardActions, Button } from '@material-ui/core'; class AuthenticationCustomComponent extends React.Component { static propTypes = { diff --git a/frontend/src/component/user/authentication-password-component.jsx b/frontend/src/component/user/authentication-password-component.jsx deleted file mode 100644 index 797b9c37a5..0000000000 --- a/frontend/src/component/user/authentication-password-component.jsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { CardActions, Button, Textfield } from 'react-mdl'; - -class EnterpriseAuthenticationComponent extends React.Component { - static propTypes = { - authDetails: PropTypes.object.isRequired, - passwordLogin: PropTypes.func.isRequired, - loadInitialData: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, - }; - - constructor() { - super(); - this.state = {}; - } - - onShowOptions = e => { - e.preventDefault(); - this.setState({ showFields: true }); - }; - - handleSubmit = async evt => { - evt.preventDefault(); - const { username, password } = this.state; - - if (!username) { - this.setState({ usernameError: 'This is a required field' }); - return; - } - if (!password) { - this.setState({ passwordError: 'This is a required field' }); - return; - } - const user = { username, password }; - const path = evt.target.action; - - try { - await this.props.passwordLogin(path, user); - await this.props.loadInitialData(); - this.props.history.push(`/`); - } catch (error) { - if (error.statusCode === 404) { - this.setState({ error: 'User not found', password: '' }); - } else if (error.statusCode === 400) { - this.setState({ error: 'Wrong password', password: '' }); - } else { - this.setState({ error: 'Could not sign in at the moment.' }); - } - } - }; - - renderLoginForm() { - const authDetails = this.props.authDetails; - const { username, usernameError, password, passwordError, error } = this.state; - return ( -
    -

    {authDetails.message}

    -

    {error}

    - this.setState({ username: evt.target.value })} - value={username} - error={usernameError} - /> - this.setState({ password: evt.target.value })} - floatingLabel - name="password" - type="password" - value={password} - error={passwordError} - /> -
    - - - - - - ); - } - - renderWithOptions(options) { - return ( -
    - {options.map(o => ( - - - - ))} - {this.state.showFields ? ( - this.renderLoginForm() - ) : ( - - Show more options - - )} -
    - ); - } - - render() { - const { options = [] } = this.props.authDetails; - if (options.length > 0) { - return this.renderWithOptions(options); - } - return this.renderLoginForm(); - } -} - -export default EnterpriseAuthenticationComponent; diff --git a/frontend/src/component/user/authentication-simple-component.jsx b/frontend/src/component/user/authentication-simple-component.jsx deleted file mode 100644 index aac76b7577..0000000000 --- a/frontend/src/component/user/authentication-simple-component.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { CardActions, Button, Textfield } from 'react-mdl'; - -class SimpleAuthenticationComponent extends React.Component { - static propTypes = { - authDetails: PropTypes.object.isRequired, - insecureLogin: PropTypes.func.isRequired, - loadInitialData: PropTypes.func.isRequired, - history: PropTypes.object.isRequired, - }; - - handleSubmit = evt => { - evt.preventDefault(); - const email = this.refs.email.inputRef.value; - const user = { email }; - const path = evt.target.action; - - this.props - .insecureLogin(path, user) - .then(this.props.loadInitialData) - .then(() => this.props.history.push(`/`)); - }; - - render() { - const authDetails = this.props.authDetails; - return ( -
    -

    {authDetails.message}

    -

    - This instance of Unleash is not set up with a secure authentication provider. You can read more - about{' '} - - securing Unleash on GitHub - -

    - -
    - - - - - - ); - } -} - -export default SimpleAuthenticationComponent; diff --git a/frontend/src/component/user/logout-component.jsx b/frontend/src/component/user/logout-component.jsx index 2f88b3eb07..edc7794220 100644 --- a/frontend/src/component/user/logout-component.jsx +++ b/frontend/src/component/user/logout-component.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Card, CardText, CardTitle } from 'react-mdl'; +import { Card, CardContent, CardHeader } from '@material-ui/core'; import { styles as commonStyles } from '../common'; export default class FeatureListComponent extends React.Component { @@ -15,8 +15,10 @@ export default class FeatureListComponent extends React.Component { render() { return ( - Logged out - You have now been successfully logged out of Unleash. Thank you for using Unleash. + Logged out + + You have now been successfully logged out of Unleash. Thank you for using Unleash.{' '} + ); } diff --git a/frontend/src/component/user/show-user-component.jsx b/frontend/src/component/user/show-user-component.jsx index 71a8b23a76..2424c5cf25 100644 --- a/frontend/src/component/user/show-user-component.jsx +++ b/frontend/src/component/user/show-user-component.jsx @@ -1,7 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import styles from './user.module.scss'; -import { Menu, MenuItem } from 'react-mdl'; +import { MenuItem, Avatar, Typography, Icon } from '@material-ui/core'; +import DropdownMenu from '../common/dropdown-menu'; export default class ShowUserComponent extends React.Component { static propTypes = { @@ -29,7 +30,10 @@ export default class ShowUserComponent extends React.Component { const locale = navigator.language || navigator.userLanguage; let found = this.possibleLocales.find(l => l.value === locale); if (!found) { - this.possibleLocales.push({ value: locale, image: 'unknown-locale' }); + this.possibleLocales.push({ + value: locale, + image: 'unknown-locale', + }); } } @@ -49,22 +53,22 @@ export default class ShowUserComponent extends React.Component { const imageLocale = foundLocale ? `public/${foundLocale.image}.png` : `public/unknown-locale.png`; return (
    -
    - {locale} -
    - - {this.possibleLocales.map(i => ( - this.setLocale(i)}> -
    - {i.value} -
    -
    - ))} -
    -   -
    - {email} -
    + } + renderOptions={() => + this.possibleLocales.map(i => ( + this.setLocale(i)}> +
    + {i.value} + {i.value} +
    +
    + )) + } + label="Locale" + /> +
    ); } diff --git a/frontend/src/component/user/user.module.scss b/frontend/src/component/user/user.module.scss index 522ea55d8d..9adb8f20fd 100644 --- a/frontend/src/component/user/user.module.scss +++ b/frontend/src/component/user/user.module.scss @@ -4,13 +4,32 @@ border: 2px solid #ffffff; } +.showLocale { + display: flex; + align-items: center; +} + +.labelFlag, .showLocale img { border-radius: 2px; - height: 30px; + width: 30px; + height: 18px; margin: 0 10px; } +.avatar { + width: 30px; + height: 30px; + border-radius: 5px; + margin-left: 5px; +} + .showUserSettings { display: flex; align-items: center; } + +.dropdown { + color: #fff; + font-weight: normal; +} \ No newline at end of file diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index f3bb8901d2..75302b5a2e 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -1,16 +1,17 @@ import 'whatwg-fetch'; -import 'react-mdl/extra/material.js'; -import 'react-mdl/extra/css/material.blue_grey-pink.min.css'; import './app.css'; import React from 'react'; import ReactDOM from 'react-dom'; import { HashRouter, Route } from 'react-router-dom'; import { Provider } from 'react-redux'; +import { ThemeProvider, CssBaseline } from '@material-ui/core'; import thunkMiddleware from 'redux-thunk'; import { createStore, applyMiddleware, compose } from 'redux'; +import { StylesProvider } from '@material-ui/core/styles'; +import mainTheme from './themes/main-theme'; import store from './store'; import MetricsPoller from './metrics-poller'; import App from './component/app'; @@ -33,9 +34,14 @@ metricsPoller.start(); ReactDOM.render( - - - + + + + + + + + , document.getElementById('app') diff --git a/frontend/src/page/addons/index.js b/frontend/src/page/addons/index.js index 1b154129d8..be6ab3f384 100644 --- a/frontend/src/page/addons/index.js +++ b/frontend/src/page/addons/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import Addons from '../../component/addons/list-container'; +import Addons from '../../component/addons'; import PropTypes from 'prop-types'; const render = ({ history }) => ; diff --git a/frontend/src/page/admin/admin-menu.jsx b/frontend/src/page/admin/admin-menu.jsx index 1aa660546a..7ea0eaeb6e 100644 --- a/frontend/src/page/admin/admin-menu.jsx +++ b/frontend/src/page/admin/admin-menu.jsx @@ -1,12 +1,52 @@ import React from 'react'; -import { Link } from 'react-router-dom'; +import { NavLink } from 'react-router-dom'; +import { Grid, Icon } from '@material-ui/core'; +import PageContent from '../../component/common/PageContent/PageContent'; + +const navLinkStyle = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: '100%', + textDecoration: 'none', + color: 'inherit', + padding: '0.8rem 1.5rem', +}; + +const activeNavLinkStyle = { + fontWeight: 'bold', + borderRadius: '3px', + padding: '0.8rem 1.5rem', +}; + +const iconStyle = { + marginRight: '5px', +}; function AdminMenu() { return ( -
    - Users | API Access |{' '} - Authentication -
    + + + + + supervised_user_circle + Users + + + + + apps + API Access + + + + + lock + Authentication + + + + ); } diff --git a/frontend/src/page/admin/api/api-howto.jsx b/frontend/src/page/admin/api/api-howto.jsx index 3a4b6d9938..ec1fe0644f 100644 --- a/frontend/src/page/admin/api/api-howto.jsx +++ b/frontend/src/page/admin/api/api-howto.jsx @@ -2,7 +2,7 @@ import React from 'react'; function ApiHowTo() { return ( -
    +

    - {show ? ( -

    - { + submit(e); + setShow(false); + }} + open={show} + primaryButtonText="Create new key" + onClose={toggle} + secondaryButtonText="Cancel" + title="Add new API key" + > + + setUsername(e.target.value)} label="Username" - floatingLabel style={{ width: '200px' }} - error={error} + error={error !== undefined} + helperText={error} + variant="outlined" + size="small" /> - - - - + + + + - ) : ( - - Add new access key - - )} + +
    ); } diff --git a/frontend/src/page/admin/api/api-key-list.jsx b/frontend/src/page/admin/api/api-key-list.jsx index 755f51404e..aaeba807a5 100644 --- a/frontend/src/page/admin/api/api-key-list.jsx +++ b/frontend/src/page/admin/api/api-key-list.jsx @@ -1,17 +1,21 @@ /* eslint-disable no-alert */ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { Icon } from 'react-mdl'; +import { Icon, Table, TableHead, TableBody, TableRow, TableCell, IconButton } from '@material-ui/core'; import { formatFullDateTimeWithLocale } from '../../../component/common/util'; import CreateApiKey from './api-key-create'; import Secret from './secret'; import ApiHowTo from './api-howto'; +import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender'; +import Dialogue from '../../../component/common/Dialogue/Dialogue'; function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermission }) { - const deleteKey = async key => { - if (confirm('Are you sure?')) { - await removeKey(key); - } + const [showDelete, setShowDelete] = useState(false); + const [delKey, setDelKey] = useState(undefined); + const deleteKey = async () => { + await removeKey(delKey); + setDelKey(undefined); + setShowDelete(false); }; useEffect(() => { @@ -21,57 +25,63 @@ function ApiKeyList({ location, fetchApiKeys, removeKey, addKey, keys, hasPermis return (
    - - - - - - - - - - - +
    - Created - - Username - - Acess Type - - Secret - - Action -
    + + + Created + Username + Access Type + Secret + Action + + + {keys.map(item => ( - - - - - - {hasPermission('ADMIN') ? ( - - ) : ( - + + + { + setDelKey(item.secret); + setShowDelete(true); + }} + > + delete + + + } + /> + ))} - -
    - {formatFullDateTimeWithLocale(item.createdAt, location.locale)} - {item.username}{item.type} + + + {formatFullDateTimeWithLocale(item.created, location.locale)} + + {item.username} + {item.type} + - - { - e.preventDefault(); - deleteKey(item.secret); - }} - > - - - - )} -
    - {hasPermission('ADMIN') ? : null} + + + { + setShowDelete(false); + setDelKey(undefined); + }} + title="Really delete API key?" + > +
    Are you sure you want to delete?
    + + } + /> + } />
    ); } diff --git a/frontend/src/page/admin/api/index.js b/frontend/src/page/admin/api/index.js index 8917e9bdb3..8deb67c81d 100644 --- a/frontend/src/page/admin/api/index.js +++ b/frontend/src/page/admin/api/index.js @@ -3,12 +3,14 @@ import PropTypes from 'prop-types'; import ApiKeyList from './api-key-list-container'; import AdminMenu from '../admin-menu'; +import PageContent from '../../../component/common/PageContent/PageContent'; const render = () => (
    -

    API Access

    - + + +
    ); diff --git a/frontend/src/page/admin/api/secret.jsx b/frontend/src/page/admin/api/secret.jsx index 9af6876128..50611723c7 100644 --- a/frontend/src/page/admin/api/secret.jsx +++ b/frontend/src/page/admin/api/secret.jsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { Icon } from 'react-mdl'; +import { Icon } from '@material-ui/core'; function Secret({ value }) { const [show, setShow] = useState(false); @@ -18,7 +18,7 @@ function Secret({ value }) { )} - + visibility
    ); diff --git a/frontend/src/page/admin/api/styles.js b/frontend/src/page/admin/api/styles.js new file mode 100644 index 0000000000..cb194c5274 --- /dev/null +++ b/frontend/src/page/admin/api/styles.js @@ -0,0 +1,8 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles({ + addApiKeyForm: { + display: 'flex', + flexDirection: 'column', + }, +}); diff --git a/frontend/src/page/admin/auth/google-auth.jsx b/frontend/src/page/admin/auth/google-auth.jsx index e6194d0002..8a94db9a16 100644 --- a/frontend/src/page/admin/auth/google-auth.jsx +++ b/frontend/src/page/admin/auth/google-auth.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Button, Grid, Cell, Switch, Textfield } from 'react-mdl'; +import { Button, Grid, Switch, TextField, Typography } from '@material-ui/core'; +import PageContent from '../../../component/common/PageContent/PageContent'; const initialState = { enabled: false, @@ -53,71 +54,72 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission } }; return ( -
    + ); } diff --git a/frontend/src/page/admin/auth/index.js b/frontend/src/page/admin/auth/index.js index 5a47ebd760..056c606455 100644 --- a/frontend/src/page/admin/auth/index.js +++ b/frontend/src/page/admin/auth/index.js @@ -1,24 +1,29 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { Tabs, Tab } from 'react-mdl'; import AdminMenu from '../admin-menu'; import GoogleAuth from './google-auth-container'; import SamlAuth from './saml-auth-container'; +import TabNav from '../../../component/common/TabNav/TabNav'; +import PageContent from '../../../component/common/PageContent/PageContent'; function AdminAuthPage() { - const [activeTab, setActiveTab] = useState(0); + const tabs = [ + { + label: 'SAML 2.0', + component: , + }, + { + label: 'Google', + component: , + }, + ]; return (
    -

    Authentication

    -
    - - SAML 2.0 - Google - -
    {activeTab === 0 ? : }
    -
    + + +
    ); } diff --git a/frontend/src/page/admin/auth/saml-auth.jsx b/frontend/src/page/admin/auth/saml-auth.jsx index 1690af3984..008c774248 100644 --- a/frontend/src/page/admin/auth/saml-auth.jsx +++ b/frontend/src/page/admin/auth/saml-auth.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Button, Grid, Cell, Switch, Textfield } from 'react-mdl'; +import { Button, Grid, Switch, TextField, Typography } from '@material-ui/core'; +import PageContent from '../../../component/common/PageContent/PageContent'; const initialState = { enabled: false, @@ -53,123 +54,129 @@ function SamlAuth({ config, getSamlConfig, updateSamlConfig, hasPermission }) { } }; return ( -
    - - -

    + + + + Please read the{' '} documentation {' '} - to learn how to integrate with specific SMAL 2.0 providers (Okta, Keycloak, etc).
    -
    + to learn how to integrate with specific SAML 2.0 providers (Okta, Keycloak, etc).
    Callback URL: https://[unleash.hostname.com]/auth/saml/callback -

    -
    + +
    - - + + Enable

    Enable SAML 2.0 Authentication.

    -
    - - +
    + + {data.enabled ? 'Enabled' : 'Disabled'} - + - - + + Entity ID

    (Required) The Entity Identity provider issuer.

    -
    - - + + - +
    - - + + Single Sign-On URL

    (Required) The url to redirect the user to for signing in.

    -
    - - + + - +
    - - + + X.509 Certificate

    (Required) The certificate used to sign the SAML 2.0 request.

    -
    - -
    - - -

    + + + + Please read the{' '} documentation {' '} to learn how to integrate with Google OAuth 2.0.
    -
    Callback URL: https://[unleash.hostname.com]/auth/google/callback -

    -
    + +
    - - + + Enable

    Enable Google users to login. Value is ignored if Client ID and Client Secret are not defined.

    -
    - +
    + {data.enabled ? 'Enabled' : 'Disabled'} - + - - + + Client ID

    (Required) The Client ID provided by Google when registering the application.

    -
    - - + + - +
    - - + + Client Secret

    (Required) Client Secret provided by Google when registering the application.

    -
    - - + + - +
    - - + + Unleash hostname

    (Required) The hostname you are running Unleash on that Google should send the user back to. @@ -126,58 +128,61 @@ function GoogleAuth({ config, getGoogleConfig, updateGoogleConfig, hasPermission https://[unleash.hostname.com]/auth/google/callback

    - - - + + - + - - + + Auto-create users

    Enable automatic creation of new users when signing in with Google.

    -
    - +
    + Auto-create users - + - - + + Email domains

    (Optional) Comma separated list of email domains that should be allowed to sign in.

    -
    - - + + - +
    - - - {' '} {info} - +
    -