From 7d89e519a26284bc26d0159e39959b70c87ba232 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Fri, 19 Oct 2018 16:17:02 +0100 Subject: [PATCH] UI: New ACLs (#4789) UI to accompany the new ACLs APIs --- ui-v2/README.md | 27 +- ui-v2/app/adapters/application.js | 4 +- ui-v2/app/adapters/policy.js | 73 +++ ui-v2/app/adapters/token.js | 199 ++++++++ ui-v2/app/components/app-view.js | 8 +- ui-v2/app/components/code-editor.js | 8 +- ui-v2/app/components/copy-button-feedback.js | 3 + ui-v2/app/components/delete-confirmation.js | 7 + ui-v2/app/components/dom-buffer-flush.js | 19 + ui-v2/app/components/dom-buffer.js | 17 + ui-v2/app/components/feedback-dialog.js | 2 +- ui-v2/app/components/list-collection.js | 2 +- ui-v2/app/components/modal-dialog.js | 119 +++++ ui-v2/app/components/modal-layer.js | 18 + ui-v2/app/components/secret-button.js | 3 + ui-v2/app/components/tabular-collection.js | 57 +-- ui-v2/app/components/tabular-details.js | 17 + ui-v2/app/components/token-list.js | 6 + .../controllers/dc/acls/policies/create.js | 2 + .../app/controllers/dc/acls/policies/edit.js | 45 ++ .../app/controllers/dc/acls/policies/index.js | 23 + .../app/controllers/dc/acls/tokens/create.js | 2 + ui-v2/app/controllers/dc/acls/tokens/edit.js | 79 ++++ ui-v2/app/controllers/dc/acls/tokens/index.js | 33 ++ ui-v2/app/controllers/dc/nodes/show.js | 2 +- ui-v2/app/forms/policy.js | 10 + ui-v2/app/forms/token.js | 9 + ui-v2/app/helpers/difference.js | 9 + ui-v2/app/helpers/policy/datacenters.js | 16 + ui-v2/app/helpers/policy/is-management.js | 8 + ui-v2/app/helpers/token/is-anonymous.js | 8 + ui-v2/app/helpers/token/is-legacy.js | 9 + ui-v2/app/initializers/form.js | 19 + ui-v2/app/initializers/ivy-codemirror.js | 11 + ui-v2/app/mixins/creating-route.js | 24 + ui-v2/app/mixins/policy/with-actions.js | 4 + ui-v2/app/mixins/token/with-actions.js | 61 +++ ui-v2/app/mixins/with-blocking-actions.js | 4 +- ui-v2/app/models/policy.js | 29 ++ ui-v2/app/models/service.js | 1 + ui-v2/app/models/token.js | 47 ++ ui-v2/app/router.js | 10 +- ui-v2/app/routes/application.js | 15 + ui-v2/app/routes/dc/acls.js | 31 ++ ui-v2/app/routes/dc/acls/index.js | 3 + ui-v2/app/routes/dc/acls/policies/create.js | 6 + ui-v2/app/routes/dc/acls/policies/edit.js | 37 ++ ui-v2/app/routes/dc/acls/policies/index.js | 29 ++ ui-v2/app/routes/dc/acls/tokens/create.js | 6 + ui-v2/app/routes/dc/acls/tokens/edit.js | 105 +++++ ui-v2/app/routes/dc/acls/tokens/index.js | 29 ++ ui-v2/app/routes/dc/kv/index.js | 8 +- ui-v2/app/routes/settings.js | 2 +- ui-v2/app/routing/single.js | 28 ++ ui-v2/app/serializers/policy.js | 7 + ui-v2/app/serializers/token.js | 7 + ui-v2/app/services/dom-buffer.js | 21 + ui-v2/app/services/dom.js | 54 +++ ui-v2/app/services/feedback.js | 14 +- ui-v2/app/services/form.js | 10 + ui-v2/app/services/policies.js | 58 +++ ui-v2/app/services/settings.js | 34 +- ui-v2/app/services/store.js | 7 + ui-v2/app/services/tokens.js | 72 +++ ui-v2/app/styles/app.scss | 29 +- ui-v2/app/styles/base/icons/index.scss | 11 +- .../app/styles/base/reset/base-variables.scss | 7 + ui-v2/app/styles/base/reset/system.scss | 4 +- .../base/typography/base-variables.scss | 8 +- ui-v2/app/styles/base/typography/index.scss | 1 + .../base/typography/semantic-variables.scss | 3 + .../components/action-group/layout.scss | 3 +- .../styles/components/action-group/skin.scss | 24 +- ui-v2/app/styles/components/anchors.scss | 10 +- ui-v2/app/styles/components/app-view.scss | 40 ++ .../app/styles/components/app-view/index.scss | 2 + .../styles/components/app-view/layout.scss | 52 +++ .../app/styles/components/app-view/skin.scss | 21 + .../styles/components/breadcrumbs/layout.scss | 2 +- .../app/styles/components/buttons/layout.scss | 33 +- ui-v2/app/styles/components/buttons/skin.scss | 4 + .../components/checkbox-group/index.scss | 2 + .../components/checkbox-group/layout.scss | 9 + .../components/checkbox-group/skin.scss | 3 + .../components/confirmation-dialog.scss | 2 +- .../confirmation-dialog/layout.scss | 5 +- .../components/dom-recycling-table/index.scss | 1 + .../dom-recycling-table/layout.scss | 13 + .../app/styles/components/flash-message.scss | 3 + .../app/styles/components/form-elements.scss | 30 +- .../components/form-elements/layout.scss | 40 +- .../styles/components/form-elements/skin.scss | 9 +- ui-v2/app/styles/components/icons/index.scss | 53 ++- ui-v2/app/styles/components/index.scss | 31 ++ ui-v2/app/styles/components/modal-dialog.scss | 13 + .../styles/components/modal-dialog/index.scss | 2 + .../components/modal-dialog/layout.scss | 70 +++ .../styles/components/modal-dialog/skin.scss | 42 ++ ui-v2/app/styles/components/notice.scss | 6 + .../app/styles/components/notice/layout.scss | 11 +- ui-v2/app/styles/components/notice/skin.scss | 15 + ui-v2/app/styles/components/product.scss | 14 - .../styles/components/product/app-view.scss | 87 ---- .../app/styles/components/product/footer.scss | 8 +- .../app/styles/components/product/index.scss | 1 - ui-v2/app/styles/components/progress.scss | 4 + .../app/styles/components/progress/index.scss | 2 + .../styles/components/progress/layout.scss | 13 + .../app/styles/components/progress/skin.scss | 6 + .../styles/components/radio-group/index.scss | 1 + .../styles/components/radio-group/layout.scss | 23 + .../app/styles/components/secret-button.scss | 11 + .../components/secret-button/index.scss | 2 + .../components/secret-button/layout.scss | 23 + .../styles/components/secret-button/skin.scss | 0 ui-v2/app/styles/components/table.scss | 124 +---- ui-v2/app/styles/components/table/index.scss | 2 + ui-v2/app/styles/components/table/layout.scss | 124 +++++ ui-v2/app/styles/components/table/skin.scss | 13 + .../styles/components/tabular-collection.scss | 125 +++-- .../styles/components/tabular-details.scss | 4 + .../components/tabular-details/index.scss | 2 + .../components/tabular-details/layout.scss | 69 +++ .../components/tabular-details/skin.scss | 20 + .../app/styles/components/toggle-button.scss | 1 + .../components/toggle-button/index.scss | 2 + .../components/toggle-button/layout.scss | 11 + .../styles/components/toggle-button/skin.scss | 11 + .../app/styles/components/toggle/layout.scss | 31 +- ui-v2/app/styles/components/toggle/skin.scss | 27 +- .../components/with-tooltip/layout.scss | 1 + ui-v2/app/styles/core/layout.scss | 12 +- ui-v2/app/styles/core/typography.scss | 15 +- ui-v2/app/styles/layouts/containers.scss | 1 + ui-v2/app/styles/routes/dc/acls/index.scss | 16 + .../styles/routes/dc/acls/policies/index.scss | 0 .../styles/routes/dc/acls/tokens/index.scss | 33 ++ ui-v2/app/styles/variables/custom-query.scss | 5 + ui-v2/app/templates/components/app-view.hbs | 21 +- .../app/templates/components/code-editor.hbs | 3 +- .../components/confirmation-dialog.hbs | 12 +- .../components/copy-button-feedback.hbs | 15 + .../app/templates/components/copy-button.hbs | 2 + .../components/delete-confirmation.hbs | 7 + .../templates/components/dom-buffer-flush.hbs | 1 + ui-v2/app/templates/components/dom-buffer.hbs | 1 + .../templates/components/hashicorp-consul.hbs | 7 +- .../app/templates/components/modal-dialog.hbs | 19 + .../app/templates/components/modal-layer.hbs | 1 + .../templates/components/secret-button.hbs | 5 + ui-v2/app/templates/components/tab-nav.hbs | 10 +- .../components/tabular-collection.hbs | 35 +- .../templates/components/tabular-details.hbs | 32 ++ ui-v2/app/templates/components/token-list.hbs | 27 ++ ui-v2/app/templates/dc/acls.hbs | 1 + .../app/templates/dc/acls/-authorization.hbs | 13 + ui-v2/app/templates/dc/acls/-disabled.hbs | 6 + ui-v2/app/templates/dc/acls/-form.hbs | 10 +- ui-v2/app/templates/dc/acls/-nav.hbs | 13 + .../templates/dc/acls/policies/-fieldsets.hbs | 49 ++ .../app/templates/dc/acls/policies/-form.hbs | 46 ++ .../dc/acls/policies/-notifications.hbs | 20 + .../app/templates/dc/acls/policies/-view.hbs | 12 + ui-v2/app/templates/dc/acls/policies/edit.hbs | 40 ++ .../app/templates/dc/acls/policies/index.hbs | 81 ++++ .../dc/acls/tokens/-fieldsets-legacy.hbs | 28 ++ .../templates/dc/acls/tokens/-fieldsets.hbs | 20 + ui-v2/app/templates/dc/acls/tokens/-form.hbs | 27 ++ .../dc/acls/tokens/-notifications.hbs | 44 ++ .../templates/dc/acls/tokens/-policies.hbs | 77 +++ ui-v2/app/templates/dc/acls/tokens/edit.hbs | 76 +++ ui-v2/app/templates/dc/acls/tokens/index.hbs | 134 ++++++ ui-v2/app/templates/dc/intentions/-form.hbs | 6 +- .../{notifications.hbs => -notifications.hbs} | 0 ui-v2/app/templates/dc/intentions/index.hbs | 6 +- ui-v2/app/templates/dc/kv/-form.hbs | 16 +- ui-v2/app/templates/dc/kv/index.hbs | 6 +- ui-v2/app/templates/error.hbs | 4 +- ui-v2/app/utils/acls-status.js | 48 ++ ui-v2/app/utils/dom/click-first-anchor.js | 30 ++ ui-v2/app/utils/dom/closest.js | 10 + ui-v2/app/utils/dom/normalize-event.js | 8 + ui-v2/app/utils/{ => dom}/qsa-factory.js | 0 ui-v2/app/utils/dom/sibling.js | 10 + ui-v2/app/utils/form/builder.js | 156 +++++++ ui-v2/app/utils/get-form-name-property.js | 6 + ui-v2/app/utils/http/status.js | 1 + ui-v2/app/utils/storage/local-storage.js | 45 ++ ui-v2/app/utils/templatize.js | 3 + ui-v2/app/utils/update-array-object.js | 12 + ui-v2/app/validations/policy.js | 4 + ui-v2/app/validations/token.js | 1 + ui-v2/ember-cli-build.js | 7 +- ui-v2/lib/startup/index.js | 2 +- ui-v2/package.json | 3 +- .../acceptance/components/acl-filter.feature | 1 + ui-v2/tests/acceptance/dc/acls/index.feature | 18 +- .../acceptance/dc/acls/list-order.feature | 1 + .../acceptance/dc/acls/policies/index.feature | 16 + .../dc/acls/policies/update.feature | 46 ++ .../dc/acls/policies/view-management.feature | 20 + .../acls/tokens/anonymous-no-delete.feature | 25 + .../acceptance/dc/acls/tokens/index.feature | 13 + .../dc/acls/tokens/legacy/update.feature | 56 +++ .../acls/tokens/policies/add-existing.feature | 46 ++ .../dc/acls/tokens/policies/add-new.feature | 45 ++ .../dc/acls/tokens/policies/list.feature | 22 + .../dc/acls/tokens/policies/remove.feature | 29 ++ .../acceptance/dc/acls/tokens/update.feature | 40 ++ .../acceptance/dc/acls/tokens/use.feature | 45 ++ ui-v2/tests/acceptance/dc/acls/update.feature | 1 + ui-v2/tests/acceptance/dc/acls/use.feature | 1 + .../acceptance/dc/intentions/update.feature | 6 +- ui-v2/tests/acceptance/dc/list.feature | 6 +- ui-v2/tests/acceptance/deleting.feature | 33 +- .../tests/acceptance/page-navigation.feature | 73 ++- .../tests/acceptance/settings/update.feature | 5 +- .../steps/dc/acls/policies/index-steps.js | 11 + .../steps/dc/acls/policies/update-steps.js | 11 + .../dc/acls/policies/view-management-steps.js | 11 + .../acls/tokens/anonymous-no-delete-steps.js | 10 + .../steps/dc/acls/tokens/index-steps.js | 11 + .../dc/acls/tokens/legacy/update-steps.js | 10 + .../tokens/policies/add-existing-steps.js | 11 + .../dc/acls/tokens/policies/add-new-steps.js | 10 + .../dc/acls/tokens/policies/list-steps.js | 11 + .../dc/acls/tokens/policies/remove-steps.js | 11 + .../steps/dc/acls/tokens/update-steps.js | 10 + .../steps/dc/acls/tokens/use-steps.js | 11 + ui-v2/tests/acceptance/token-header.feature | 1 + ui-v2/tests/helpers/api.js | 1 + ui-v2/tests/helpers/set-cookies.js | 11 +- ui-v2/tests/helpers/type-to-url.js | 6 + .../adapters/policy/response-test.js | 38 ++ .../integration/adapters/policy/url-test.js | 76 +++ .../adapters/token/response-test.js | 38 ++ .../integration/adapters/token/url-test.js | 84 ++++ .../components/copy-button-feedback-test.js | 32 ++ .../components/delete-confirmation-test.js | 22 + .../components/dom-buffer-flush-test.js | 34 ++ .../integration/components/dom-buffer-test.js | 34 ++ .../components/modal-dialog-test.js | 33 ++ .../components/modal-layer-test.js | 22 + .../components/secret-button-test.js | 33 ++ .../components/tabular-details-test.js | 33 ++ .../integration/components/token-list-test.js | 33 ++ .../helpers/policy/datacenters-test.js | 20 + .../helpers/policy/is-management-test.js | 20 + .../helpers/token/is-anonymous-test.js | 20 + .../helpers/token/is-legacy-test.js | 20 + .../integration/services/policies-test.js | 68 +++ .../tests/integration/services/tokens-test.js | 64 +++ .../tests/lib/page-object/createCancelable.js | 9 +- .../tests/lib/page-object/createSubmitable.js | 9 +- ui-v2/tests/pages.js | 25 +- .../tests/pages/components/freetext-filter.js | 4 + ui-v2/tests/pages/dc/acls/policies/edit.js | 16 + ui-v2/tests/pages/dc/acls/policies/index.js | 14 + ui-v2/tests/pages/dc/acls/tokens/edit.js | 36 ++ ui-v2/tests/pages/dc/acls/tokens/index.js | 16 + ui-v2/tests/steps.js | 96 +++- ui-v2/tests/unit/adapters/policy-test.js | 12 + ui-v2/tests/unit/adapters/token-test.js | 12 + .../unit/controllers/dc/acls/create-test.js | 12 + .../dc/acls/policies/create-test.js | 12 + .../controllers/dc/acls/policies/edit-test.js | 12 + .../dc/acls/policies/index-test.js | 12 + .../controllers/dc/acls/tokens/create-test.js | 12 + .../controllers/dc/acls/tokens/edit-test.js | 12 + .../controllers/dc/acls/tokens/index-test.js | 12 + .../unit/helpers/policy/datacenters-test.js | 26 ++ .../unit/helpers/policy/is-management-test.js | 13 + .../unit/helpers/token/is-anonymous-test.js | 13 + .../tests/unit/mixins/creating-route-test.js | 12 + .../unit/mixins/policy/with-actions-test.js | 29 ++ .../unit/mixins/token/with-actions-test.js | 29 ++ ui-v2/tests/unit/models/policy-test.js | 14 + ui-v2/tests/unit/models/token-test.js | 14 + ui-v2/tests/unit/routes/dc/acls-test.js | 17 + .../routes/dc/acls/policies/create-test.js | 19 + .../unit/routes/dc/acls/policies/edit-test.js | 19 + .../routes/dc/acls/policies/index-test.js | 17 + .../unit/routes/dc/acls/tokens/create-test.js | 19 + .../unit/routes/dc/acls/tokens/edit-test.js | 19 + .../unit/routes/dc/acls/tokens/index-test.js | 17 + ui-v2/tests/unit/serializers/policy-test.js | 24 + ui-v2/tests/unit/serializers/token-test.js | 24 + ui-v2/tests/unit/services/dom-buffer-test.js | 12 + ui-v2/tests/unit/services/dom-test.js | 12 + ui-v2/tests/unit/services/form-test.js | 12 + ui-v2/tests/unit/utils/acls-status-test.js | 10 + .../unit/utils/dom/click-first-anchor-test.js | 63 +++ ui-v2/tests/unit/utils/dom/closest-test.js | 22 + .../unit/utils/dom/normalize-event-test.js | 15 + .../unit/utils/{ => dom}/qsa-factory-test.js | 2 +- ui-v2/tests/unit/utils/dom/sibling-test.js | 62 +++ .../unit/utils/get-form-name-property-test.js | 11 + .../unit/utils/storage/local-storage-test.js | 68 +++ ui-v2/tests/unit/utils/templatize-test.js | 20 + .../unit/utils/update-array-object-test.js | 30 ++ ui-v2/yarn.lock | 442 +++++++++++++++--- 301 files changed, 6548 insertions(+), 684 deletions(-) create mode 100644 ui-v2/app/adapters/policy.js create mode 100644 ui-v2/app/adapters/token.js create mode 100644 ui-v2/app/components/copy-button-feedback.js create mode 100644 ui-v2/app/components/delete-confirmation.js create mode 100644 ui-v2/app/components/dom-buffer-flush.js create mode 100644 ui-v2/app/components/dom-buffer.js create mode 100644 ui-v2/app/components/modal-dialog.js create mode 100644 ui-v2/app/components/modal-layer.js create mode 100644 ui-v2/app/components/secret-button.js create mode 100644 ui-v2/app/components/tabular-details.js create mode 100644 ui-v2/app/components/token-list.js create mode 100644 ui-v2/app/controllers/dc/acls/policies/create.js create mode 100644 ui-v2/app/controllers/dc/acls/policies/edit.js create mode 100644 ui-v2/app/controllers/dc/acls/policies/index.js create mode 100644 ui-v2/app/controllers/dc/acls/tokens/create.js create mode 100644 ui-v2/app/controllers/dc/acls/tokens/edit.js create mode 100644 ui-v2/app/controllers/dc/acls/tokens/index.js create mode 100644 ui-v2/app/forms/policy.js create mode 100644 ui-v2/app/forms/token.js create mode 100644 ui-v2/app/helpers/difference.js create mode 100644 ui-v2/app/helpers/policy/datacenters.js create mode 100644 ui-v2/app/helpers/policy/is-management.js create mode 100644 ui-v2/app/helpers/token/is-anonymous.js create mode 100644 ui-v2/app/helpers/token/is-legacy.js create mode 100644 ui-v2/app/initializers/form.js create mode 100644 ui-v2/app/initializers/ivy-codemirror.js create mode 100644 ui-v2/app/mixins/creating-route.js create mode 100644 ui-v2/app/mixins/policy/with-actions.js create mode 100644 ui-v2/app/mixins/token/with-actions.js create mode 100644 ui-v2/app/models/policy.js create mode 100644 ui-v2/app/models/token.js create mode 100644 ui-v2/app/routes/dc/acls.js create mode 100644 ui-v2/app/routes/dc/acls/policies/create.js create mode 100644 ui-v2/app/routes/dc/acls/policies/edit.js create mode 100644 ui-v2/app/routes/dc/acls/policies/index.js create mode 100644 ui-v2/app/routes/dc/acls/tokens/create.js create mode 100644 ui-v2/app/routes/dc/acls/tokens/edit.js create mode 100644 ui-v2/app/routes/dc/acls/tokens/index.js create mode 100644 ui-v2/app/routing/single.js create mode 100644 ui-v2/app/serializers/policy.js create mode 100644 ui-v2/app/serializers/token.js create mode 100644 ui-v2/app/services/dom-buffer.js create mode 100644 ui-v2/app/services/dom.js create mode 100644 ui-v2/app/services/form.js create mode 100644 ui-v2/app/services/policies.js create mode 100644 ui-v2/app/services/tokens.js create mode 100644 ui-v2/app/styles/base/typography/semantic-variables.scss create mode 100644 ui-v2/app/styles/components/app-view.scss create mode 100644 ui-v2/app/styles/components/app-view/index.scss create mode 100644 ui-v2/app/styles/components/app-view/layout.scss create mode 100644 ui-v2/app/styles/components/app-view/skin.scss create mode 100644 ui-v2/app/styles/components/checkbox-group/index.scss create mode 100644 ui-v2/app/styles/components/checkbox-group/layout.scss create mode 100644 ui-v2/app/styles/components/checkbox-group/skin.scss create mode 100644 ui-v2/app/styles/components/dom-recycling-table/index.scss create mode 100644 ui-v2/app/styles/components/dom-recycling-table/layout.scss create mode 100644 ui-v2/app/styles/components/index.scss create mode 100644 ui-v2/app/styles/components/modal-dialog.scss create mode 100644 ui-v2/app/styles/components/modal-dialog/index.scss create mode 100644 ui-v2/app/styles/components/modal-dialog/layout.scss create mode 100644 ui-v2/app/styles/components/modal-dialog/skin.scss delete mode 100644 ui-v2/app/styles/components/product/app-view.scss create mode 100644 ui-v2/app/styles/components/progress.scss create mode 100644 ui-v2/app/styles/components/progress/index.scss create mode 100644 ui-v2/app/styles/components/progress/layout.scss create mode 100644 ui-v2/app/styles/components/progress/skin.scss create mode 100644 ui-v2/app/styles/components/radio-group/index.scss create mode 100644 ui-v2/app/styles/components/radio-group/layout.scss create mode 100644 ui-v2/app/styles/components/secret-button.scss create mode 100644 ui-v2/app/styles/components/secret-button/index.scss create mode 100644 ui-v2/app/styles/components/secret-button/layout.scss create mode 100644 ui-v2/app/styles/components/secret-button/skin.scss create mode 100644 ui-v2/app/styles/components/table/index.scss create mode 100644 ui-v2/app/styles/components/table/layout.scss create mode 100644 ui-v2/app/styles/components/table/skin.scss create mode 100644 ui-v2/app/styles/components/tabular-details.scss create mode 100644 ui-v2/app/styles/components/tabular-details/index.scss create mode 100644 ui-v2/app/styles/components/tabular-details/layout.scss create mode 100644 ui-v2/app/styles/components/tabular-details/skin.scss create mode 100644 ui-v2/app/styles/components/toggle-button.scss create mode 100644 ui-v2/app/styles/components/toggle-button/index.scss create mode 100644 ui-v2/app/styles/components/toggle-button/layout.scss create mode 100644 ui-v2/app/styles/components/toggle-button/skin.scss create mode 100644 ui-v2/app/styles/routes/dc/acls/policies/index.scss create mode 100644 ui-v2/app/styles/routes/dc/acls/tokens/index.scss create mode 100644 ui-v2/app/templates/components/copy-button-feedback.hbs create mode 100644 ui-v2/app/templates/components/copy-button.hbs create mode 100644 ui-v2/app/templates/components/delete-confirmation.hbs create mode 100644 ui-v2/app/templates/components/dom-buffer-flush.hbs create mode 100644 ui-v2/app/templates/components/dom-buffer.hbs create mode 100644 ui-v2/app/templates/components/modal-dialog.hbs create mode 100644 ui-v2/app/templates/components/modal-layer.hbs create mode 100644 ui-v2/app/templates/components/secret-button.hbs create mode 100644 ui-v2/app/templates/components/tabular-details.hbs create mode 100644 ui-v2/app/templates/components/token-list.hbs create mode 100644 ui-v2/app/templates/dc/acls.hbs create mode 100644 ui-v2/app/templates/dc/acls/-authorization.hbs create mode 100644 ui-v2/app/templates/dc/acls/-disabled.hbs create mode 100644 ui-v2/app/templates/dc/acls/-nav.hbs create mode 100644 ui-v2/app/templates/dc/acls/policies/-fieldsets.hbs create mode 100644 ui-v2/app/templates/dc/acls/policies/-form.hbs create mode 100644 ui-v2/app/templates/dc/acls/policies/-notifications.hbs create mode 100644 ui-v2/app/templates/dc/acls/policies/-view.hbs create mode 100644 ui-v2/app/templates/dc/acls/policies/edit.hbs create mode 100644 ui-v2/app/templates/dc/acls/policies/index.hbs create mode 100644 ui-v2/app/templates/dc/acls/tokens/-fieldsets-legacy.hbs create mode 100644 ui-v2/app/templates/dc/acls/tokens/-fieldsets.hbs create mode 100644 ui-v2/app/templates/dc/acls/tokens/-form.hbs create mode 100644 ui-v2/app/templates/dc/acls/tokens/-notifications.hbs create mode 100644 ui-v2/app/templates/dc/acls/tokens/-policies.hbs create mode 100644 ui-v2/app/templates/dc/acls/tokens/edit.hbs create mode 100644 ui-v2/app/templates/dc/acls/tokens/index.hbs rename ui-v2/app/templates/dc/intentions/{notifications.hbs => -notifications.hbs} (100%) create mode 100644 ui-v2/app/utils/acls-status.js create mode 100644 ui-v2/app/utils/dom/click-first-anchor.js create mode 100644 ui-v2/app/utils/dom/closest.js create mode 100644 ui-v2/app/utils/dom/normalize-event.js rename ui-v2/app/utils/{ => dom}/qsa-factory.js (100%) create mode 100644 ui-v2/app/utils/dom/sibling.js create mode 100644 ui-v2/app/utils/form/builder.js create mode 100644 ui-v2/app/utils/get-form-name-property.js create mode 100644 ui-v2/app/utils/storage/local-storage.js create mode 100644 ui-v2/app/utils/templatize.js create mode 100644 ui-v2/app/utils/update-array-object.js create mode 100644 ui-v2/app/validations/policy.js create mode 100644 ui-v2/app/validations/token.js create mode 100644 ui-v2/tests/acceptance/dc/acls/policies/index.feature create mode 100644 ui-v2/tests/acceptance/dc/acls/policies/update.feature create mode 100644 ui-v2/tests/acceptance/dc/acls/policies/view-management.feature create mode 100644 ui-v2/tests/acceptance/dc/acls/tokens/anonymous-no-delete.feature create mode 100644 ui-v2/tests/acceptance/dc/acls/tokens/index.feature create mode 100644 ui-v2/tests/acceptance/dc/acls/tokens/legacy/update.feature create mode 100644 ui-v2/tests/acceptance/dc/acls/tokens/policies/add-existing.feature create mode 100644 ui-v2/tests/acceptance/dc/acls/tokens/policies/add-new.feature create mode 100644 ui-v2/tests/acceptance/dc/acls/tokens/policies/list.feature create mode 100644 ui-v2/tests/acceptance/dc/acls/tokens/policies/remove.feature create mode 100644 ui-v2/tests/acceptance/dc/acls/tokens/update.feature create mode 100644 ui-v2/tests/acceptance/dc/acls/tokens/use.feature create mode 100644 ui-v2/tests/acceptance/steps/dc/acls/policies/index-steps.js create mode 100644 ui-v2/tests/acceptance/steps/dc/acls/policies/update-steps.js create mode 100644 ui-v2/tests/acceptance/steps/dc/acls/policies/view-management-steps.js create mode 100644 ui-v2/tests/acceptance/steps/dc/acls/tokens/anonymous-no-delete-steps.js create mode 100644 ui-v2/tests/acceptance/steps/dc/acls/tokens/index-steps.js create mode 100644 ui-v2/tests/acceptance/steps/dc/acls/tokens/legacy/update-steps.js create mode 100644 ui-v2/tests/acceptance/steps/dc/acls/tokens/policies/add-existing-steps.js create mode 100644 ui-v2/tests/acceptance/steps/dc/acls/tokens/policies/add-new-steps.js create mode 100644 ui-v2/tests/acceptance/steps/dc/acls/tokens/policies/list-steps.js create mode 100644 ui-v2/tests/acceptance/steps/dc/acls/tokens/policies/remove-steps.js create mode 100644 ui-v2/tests/acceptance/steps/dc/acls/tokens/update-steps.js create mode 100644 ui-v2/tests/acceptance/steps/dc/acls/tokens/use-steps.js create mode 100644 ui-v2/tests/integration/adapters/policy/response-test.js create mode 100644 ui-v2/tests/integration/adapters/policy/url-test.js create mode 100644 ui-v2/tests/integration/adapters/token/response-test.js create mode 100644 ui-v2/tests/integration/adapters/token/url-test.js create mode 100644 ui-v2/tests/integration/components/copy-button-feedback-test.js create mode 100644 ui-v2/tests/integration/components/delete-confirmation-test.js create mode 100644 ui-v2/tests/integration/components/dom-buffer-flush-test.js create mode 100644 ui-v2/tests/integration/components/dom-buffer-test.js create mode 100644 ui-v2/tests/integration/components/modal-dialog-test.js create mode 100644 ui-v2/tests/integration/components/modal-layer-test.js create mode 100644 ui-v2/tests/integration/components/secret-button-test.js create mode 100644 ui-v2/tests/integration/components/tabular-details-test.js create mode 100644 ui-v2/tests/integration/components/token-list-test.js create mode 100644 ui-v2/tests/integration/helpers/policy/datacenters-test.js create mode 100644 ui-v2/tests/integration/helpers/policy/is-management-test.js create mode 100644 ui-v2/tests/integration/helpers/token/is-anonymous-test.js create mode 100644 ui-v2/tests/integration/helpers/token/is-legacy-test.js create mode 100644 ui-v2/tests/integration/services/policies-test.js create mode 100644 ui-v2/tests/integration/services/tokens-test.js create mode 100644 ui-v2/tests/pages/components/freetext-filter.js create mode 100644 ui-v2/tests/pages/dc/acls/policies/edit.js create mode 100644 ui-v2/tests/pages/dc/acls/policies/index.js create mode 100644 ui-v2/tests/pages/dc/acls/tokens/edit.js create mode 100644 ui-v2/tests/pages/dc/acls/tokens/index.js create mode 100644 ui-v2/tests/unit/adapters/policy-test.js create mode 100644 ui-v2/tests/unit/adapters/token-test.js create mode 100644 ui-v2/tests/unit/controllers/dc/acls/create-test.js create mode 100644 ui-v2/tests/unit/controllers/dc/acls/policies/create-test.js create mode 100644 ui-v2/tests/unit/controllers/dc/acls/policies/edit-test.js create mode 100644 ui-v2/tests/unit/controllers/dc/acls/policies/index-test.js create mode 100644 ui-v2/tests/unit/controllers/dc/acls/tokens/create-test.js create mode 100644 ui-v2/tests/unit/controllers/dc/acls/tokens/edit-test.js create mode 100644 ui-v2/tests/unit/controllers/dc/acls/tokens/index-test.js create mode 100644 ui-v2/tests/unit/helpers/policy/datacenters-test.js create mode 100644 ui-v2/tests/unit/helpers/policy/is-management-test.js create mode 100644 ui-v2/tests/unit/helpers/token/is-anonymous-test.js create mode 100644 ui-v2/tests/unit/mixins/creating-route-test.js create mode 100644 ui-v2/tests/unit/mixins/policy/with-actions-test.js create mode 100644 ui-v2/tests/unit/mixins/token/with-actions-test.js create mode 100644 ui-v2/tests/unit/models/policy-test.js create mode 100644 ui-v2/tests/unit/models/token-test.js create mode 100644 ui-v2/tests/unit/routes/dc/acls-test.js create mode 100644 ui-v2/tests/unit/routes/dc/acls/policies/create-test.js create mode 100644 ui-v2/tests/unit/routes/dc/acls/policies/edit-test.js create mode 100644 ui-v2/tests/unit/routes/dc/acls/policies/index-test.js create mode 100644 ui-v2/tests/unit/routes/dc/acls/tokens/create-test.js create mode 100644 ui-v2/tests/unit/routes/dc/acls/tokens/edit-test.js create mode 100644 ui-v2/tests/unit/routes/dc/acls/tokens/index-test.js create mode 100644 ui-v2/tests/unit/serializers/policy-test.js create mode 100644 ui-v2/tests/unit/serializers/token-test.js create mode 100644 ui-v2/tests/unit/services/dom-buffer-test.js create mode 100644 ui-v2/tests/unit/services/dom-test.js create mode 100644 ui-v2/tests/unit/services/form-test.js create mode 100644 ui-v2/tests/unit/utils/acls-status-test.js create mode 100644 ui-v2/tests/unit/utils/dom/click-first-anchor-test.js create mode 100644 ui-v2/tests/unit/utils/dom/closest-test.js create mode 100644 ui-v2/tests/unit/utils/dom/normalize-event-test.js rename ui-v2/tests/unit/utils/{ => dom}/qsa-factory-test.js (93%) create mode 100644 ui-v2/tests/unit/utils/dom/sibling-test.js create mode 100644 ui-v2/tests/unit/utils/get-form-name-property-test.js create mode 100644 ui-v2/tests/unit/utils/storage/local-storage-test.js create mode 100644 ui-v2/tests/unit/utils/templatize-test.js create mode 100644 ui-v2/tests/unit/utils/update-array-object-test.js diff --git a/ui-v2/README.md b/ui-v2/README.md index 9e6da2e0d3..fa05757996 100644 --- a/ui-v2/README.md +++ b/ui-v2/README.md @@ -19,12 +19,35 @@ You will need the following things properly installed on your computer. ## Running / Development +The source code comes with a small server that runs enough of the consul API +as a set of mocks/fixtures to be able to run the UI without having to run +consul. + * `make start-api` or `yarn start:api` (this starts a Consul API double running on http://localhost:3000) * `make start` or `yarn start` to start the ember app that connects to the above API double * Visit your app at [http://localhost:4200](http://localhost:4200). -* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). + +To enable ACLs using the mock API, use Web Inspector to set a cookie as follows: + +``` +CONSUL_ACLS_ENABLE=1 +``` + +This will enable the ACLs login page, to which you can login with any ACL +token/secret. + +You can also use a number of other cookie key/values to set various things whilst +developing the UI, such as (but not limited to): + +``` +CONSUL_SERVICE_COUNT=1000 +CONSUL_NODE_CODE=1000 +// etc etc +``` + +See `./node_modules/@hashicorp/consul-api-double` for more details. ### Code Generators @@ -33,7 +56,7 @@ Make use of the many generators for code, try `ember help generate` for more det ### Running Tests -You do not need to run `make start-api`/`yarn run start:api` to run the tests +Please note: You do not need to run `make start-api`/`yarn run start:api` to run the tests, but the same mock consul API is used. * `make test` or `yarn run test` * `make test-view` or `yarn run test:view` to view the tests running in Chrome diff --git a/ui-v2/app/adapters/application.js b/ui-v2/app/adapters/application.js index 1ce82690ad..8b3ac1b579 100644 --- a/ui-v2/app/adapters/application.js +++ b/ui-v2/app/adapters/application.js @@ -48,7 +48,9 @@ export default Adapter.extend({ }); }, cleanQuery: function(_query) { - delete _query.id; + if (typeof _query.id !== 'undefined') { + delete _query.id; + } const query = { ..._query }; delete _query[DATACENTER_QUERY_PARAM]; return query; diff --git a/ui-v2/app/adapters/policy.js b/ui-v2/app/adapters/policy.js new file mode 100644 index 0000000000..2b981fb147 --- /dev/null +++ b/ui-v2/app/adapters/policy.js @@ -0,0 +1,73 @@ +import Adapter, { + REQUEST_CREATE, + REQUEST_UPDATE, + DATACENTER_QUERY_PARAM as API_DATACENTER_KEY, +} from './application'; + +import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/policy'; +import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc'; +import { OK as HTTP_OK } from 'consul-ui/utils/http/status'; +import { PUT as HTTP_PUT } from 'consul-ui/utils/http/method'; + +export default Adapter.extend({ + urlForQuery: function(query, modelName) { + return this.appendURL('acl/policies', [], this.cleanQuery(query)); + }, + urlForQueryRecord: function(query, modelName) { + if (typeof query.id === 'undefined') { + throw new Error('You must specify an id'); + } + return this.appendURL('acl/policy', [query.id], this.cleanQuery(query)); + }, + urlForCreateRecord: function(modelName, snapshot) { + return this.appendURL('acl/policy', [], { + [API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY), + }); + }, + urlForUpdateRecord: function(id, modelName, snapshot) { + return this.appendURL('acl/policy', [snapshot.attr(SLUG_KEY)], { + [API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY), + }); + }, + urlForDeleteRecord: function(id, modelName, snapshot) { + return this.appendURL('acl/policy', [snapshot.attr(SLUG_KEY)], { + [API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY), + }); + }, + urlForTranslateRecord: function(modelName, snapshot) { + return this.appendURL('acl/policy/translate', [], {}); + }, + dataForRequest: function(params) { + const data = this._super(...arguments); + switch (params.requestType) { + case REQUEST_UPDATE: + case REQUEST_CREATE: + return data.policy; + } + return data; + }, + handleResponse: function(status, headers, payload, requestData) { + let response = payload; + if (status === HTTP_OK) { + const url = this.parseURL(requestData.url); + switch (true) { + case response === true: + response = this.handleBooleanResponse(url, response, PRIMARY_KEY, SLUG_KEY); + break; + case Array.isArray(response): + response = this.handleBatchResponse(url, response, PRIMARY_KEY, SLUG_KEY); + break; + default: + response = this.handleSingleResponse(url, response, PRIMARY_KEY, SLUG_KEY); + } + } + return this._super(status, headers, response, requestData); + }, + methodForRequest: function(params) { + switch (params.requestType) { + case REQUEST_CREATE: + return HTTP_PUT; + } + return this._super(...arguments); + }, +}); diff --git a/ui-v2/app/adapters/token.js b/ui-v2/app/adapters/token.js new file mode 100644 index 0000000000..402b1f6be5 --- /dev/null +++ b/ui-v2/app/adapters/token.js @@ -0,0 +1,199 @@ +import { inject as service } from '@ember/service'; +import Adapter, { + REQUEST_CREATE, + REQUEST_UPDATE, + DATACENTER_QUERY_PARAM as API_DATACENTER_KEY, +} from './application'; + +import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/token'; +import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc'; +import { OK as HTTP_OK } from 'consul-ui/utils/http/status'; +import { PUT as HTTP_PUT } from 'consul-ui/utils/http/method'; + +import { get } from '@ember/object'; + +const REQUEST_CLONE = 'cloneRecord'; +const REQUEST_SELF = 'querySelf'; + +export default Adapter.extend({ + store: service('store'), + cleanQuery: function(_query) { + const query = this._super(...arguments); + // TODO: Make sure policy is being passed through + delete _query.policy; + // take off the secret for /self + delete query.secret; + return query; + }, + urlForQuery: function(query, modelName) { + return this.appendURL('acl/tokens', [], this.cleanQuery(query)); + }, + urlForQueryRecord: function(query, modelName) { + if (typeof query.id === 'undefined') { + throw new Error('You must specify an id'); + } + return this.appendURL('acl/token', [query.id], this.cleanQuery(query)); + }, + urlForQuerySelf: function(query, modelName) { + return this.appendURL('acl/token/self', [], this.cleanQuery(query)); + }, + urlForCreateRecord: function(modelName, snapshot) { + return this.appendURL('acl/token', [], { + [API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY), + }); + }, + urlForUpdateRecord: function(id, modelName, snapshot) { + // If a token has Rules, use the old API + if (typeof snapshot.attr('Rules') !== 'undefined') { + return this.appendURL('acl/update', [], { + [API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY), + }); + } + return this.appendURL('acl/token', [snapshot.attr(SLUG_KEY)], { + [API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY), + }); + }, + urlForDeleteRecord: function(id, modelName, snapshot) { + return this.appendURL('acl/token', [snapshot.attr(SLUG_KEY)], { + [API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY), + }); + }, + urlForRequest: function({ type, snapshot, requestType }) { + switch (requestType) { + case 'cloneRecord': + return this.urlForCloneRecord(type.modelName, snapshot); + case 'querySelf': + return this.urlForQuerySelf(snapshot, type.modelName); + } + return this._super(...arguments); + }, + urlForCloneRecord: function(modelName, snapshot) { + return this.appendURL('acl/token', [snapshot.attr(SLUG_KEY), 'clone'], { + [API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY), + }); + }, + self: function(store, modelClass, snapshot) { + const params = { + store: store, + type: modelClass, + snapshot: snapshot, + requestType: 'querySelf', + }; + // _requestFor is private... but these methods aren't, until they disappear.. + const request = { + method: this.methodForRequest(params), + url: this.urlForRequest(params), + headers: this.headersForRequest(params), + data: this.dataForRequest(params), + }; + // TODO: private.. + return this._makeRequest(request); + }, + clone: function(store, modelClass, id, snapshot) { + const params = { + store: store, + type: modelClass, + id: id, + snapshot: snapshot, + requestType: 'cloneRecord', + }; + // _requestFor is private... but these methods aren't, until they disappear.. + const request = { + method: this.methodForRequest(params), + url: this.urlForRequest(params), + headers: this.headersForRequest(params), + data: this.dataForRequest(params), + }; + // TODO: private.. + return this._makeRequest(request); + }, + handleSingleResponse: function(url, response, primary, slug) { + // Sometimes we get `Policies: null`, make null equal an empty array + if (typeof response.Policies === 'undefined' || response.Policies === null) { + response.Policies = []; + } + // Convert an old style update response to a new style + if (typeof response['ID'] !== 'undefined') { + const item = get(this, 'store') + .peekAll('token') + .findBy('SecretID', response['ID']); + if (item) { + response['SecretID'] = response['ID']; + response['AccessorID'] = get(item, 'AccessorID'); + } + } + return this._super(url, response, primary, slug); + }, + handleResponse: function(status, headers, payload, requestData) { + let response = payload; + if (status === HTTP_OK) { + const url = this.parseURL(requestData.url); + switch (true) { + case response === true: + response = this.handleBooleanResponse(url, response, PRIMARY_KEY, SLUG_KEY); + break; + case Array.isArray(response): + response = this.handleBatchResponse(url, response, PRIMARY_KEY, SLUG_KEY); + break; + default: + response = this.handleSingleResponse(url, response, PRIMARY_KEY, SLUG_KEY); + } + } + return this._super(status, headers, response, requestData); + }, + methodForRequest: function(params) { + switch (params.requestType) { + case REQUEST_CLONE: + case REQUEST_CREATE: + return HTTP_PUT; + } + return this._super(...arguments); + }, + headersForRequest: function(params) { + switch (params.requestType) { + case REQUEST_SELF: + return { + 'X-Consul-Token': params.snapshot.secret, + }; + } + return this._super(...arguments); + }, + dataForRequest: function(params) { + let data = this._super(...arguments); + switch (params.requestType) { + case REQUEST_UPDATE: + // If a token has Rules, use the old API + if (typeof data.token['Rules'] !== 'undefined') { + data.token['ID'] = data.token['SecretID']; + data.token['Name'] = data.token['Description']; + } + // falls through + case REQUEST_CREATE: + if (Array.isArray(data.token.Policies)) { + data.token.Policies = data.token.Policies.filter(function(item) { + // Just incase, don't save any policies that aren't saved + return !get(item, 'isNew'); + }).map(function(item) { + return { + ID: get(item, 'ID'), + Name: get(item, 'Name'), + }; + }); + } else { + delete data.token.Policies; + } + data = data.token; + break; + case REQUEST_SELF: + return {}; + case REQUEST_CLONE: + data = {}; + break; + } + // make sure we never send the SecretID + if (data && typeof data['SecretID'] !== 'undefined') { + delete data['SecretID']; + } + return data; + }, +}); diff --git a/ui-v2/app/components/app-view.js b/ui-v2/app/components/app-view.js index 66a0a334b5..6966ba6559 100644 --- a/ui-v2/app/components/app-view.js +++ b/ui-v2/app/components/app-view.js @@ -1,14 +1,16 @@ import Component from '@ember/component'; import SlotsMixin from 'ember-block-slots'; import { get } from '@ember/object'; +import templatize from 'consul-ui/utils/templatize'; const $html = document.documentElement; -const templatize = function(arr = []) { - return arr.map(item => `template-${item}`); -}; export default Component.extend(SlotsMixin, { loading: false, + authorized: true, + enabled: true, classNames: ['app-view'], + classNameBindings: ['enabled::disabled', 'authorized::unauthorized'], didReceiveAttrs: function() { + // right now only manually added classes are hoisted to let cls = get(this, 'class') || ''; if (get(this, 'loading')) { cls += ' loading'; diff --git a/ui-v2/app/components/code-editor.js b/ui-v2/app/components/code-editor.js index 07f3712be7..a8f5560232 100644 --- a/ui-v2/app/components/code-editor.js +++ b/ui-v2/app/components/code-editor.js @@ -1,6 +1,12 @@ import Component from '@ember/component'; - +import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; +const $$ = qsaFactory(); export default Component.extend({ mode: 'application/json', + classNames: ['code-editor'], onkeyup: function() {}, + didAppear: function() { + const $editor = [...$$('textarea + div', this.element)][0]; + $editor.CodeMirror.refresh(); + }, }); diff --git a/ui-v2/app/components/copy-button-feedback.js b/ui-v2/app/components/copy-button-feedback.js new file mode 100644 index 0000000000..5570647734 --- /dev/null +++ b/ui-v2/app/components/copy-button-feedback.js @@ -0,0 +1,3 @@ +import Component from '@ember/component'; + +export default Component.extend({}); diff --git a/ui-v2/app/components/delete-confirmation.js b/ui-v2/app/components/delete-confirmation.js new file mode 100644 index 0000000000..fe5ae1b257 --- /dev/null +++ b/ui-v2/app/components/delete-confirmation.js @@ -0,0 +1,7 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', + execute: function() {}, + cancel: function() {}, +}); diff --git a/ui-v2/app/components/dom-buffer-flush.js b/ui-v2/app/components/dom-buffer-flush.js new file mode 100644 index 0000000000..035d1ac491 --- /dev/null +++ b/ui-v2/app/components/dom-buffer-flush.js @@ -0,0 +1,19 @@ +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; +import Component from '@ember/component'; +const append = function(content) { + this.element.appendChild(content); +}; +export default Component.extend({ + buffer: service('dom-buffer'), + init: function() { + this._super(...arguments); + this.append = append.bind(this); + }, + didInsertElement: function() { + get(this, 'buffer').on('add', this.append); + }, + didDestroyElement: function() { + get(this, 'buffer').off('add', this.append); + }, +}); diff --git a/ui-v2/app/components/dom-buffer.js b/ui-v2/app/components/dom-buffer.js new file mode 100644 index 0000000000..054d4ca3da --- /dev/null +++ b/ui-v2/app/components/dom-buffer.js @@ -0,0 +1,17 @@ +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; +import Component from '@ember/component'; +export default Component.extend({ + buffer: service('dom-buffer'), + getBufferName: function() { + // TODO: Right now we are only using this for the modal layer + // moving forwards you'll be able to name your buffers + return 'modal'; + }, + didInsertElement: function() { + get(this, 'buffer').add(this.getBufferName(), this.element); + }, + didDestroyElement: function() { + get(this, 'buffer').remove(this.getBufferName()); + }, +}); diff --git a/ui-v2/app/components/feedback-dialog.js b/ui-v2/app/components/feedback-dialog.js index 5f0a97ac12..e676f0d950 100644 --- a/ui-v2/app/components/feedback-dialog.js +++ b/ui-v2/app/components/feedback-dialog.js @@ -1,7 +1,7 @@ import Component from '@ember/component'; import { get, set } from '@ember/object'; import { inject as service } from '@ember/service'; -import qsaFactory from 'consul-ui/utils/qsa-factory'; +import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; const $$ = qsaFactory(); import SlotsMixin from 'ember-block-slots'; diff --git a/ui-v2/app/components/list-collection.js b/ui-v2/app/components/list-collection.js index 789c880483..f3f41f2a28 100644 --- a/ui-v2/app/components/list-collection.js +++ b/ui-v2/app/components/list-collection.js @@ -3,7 +3,7 @@ import Component from 'ember-collection/components/ember-collection'; import PercentageColumns from 'ember-collection/layouts/percentage-columns'; import style from 'ember-computed-style'; import WithResizing from 'consul-ui/mixins/with-resizing'; -import qsaFactory from 'consul-ui/utils/qsa-factory'; +import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; const $$ = qsaFactory(); export default Component.extend(WithResizing, { tagName: 'div', diff --git a/ui-v2/app/components/modal-dialog.js b/ui-v2/app/components/modal-dialog.js new file mode 100644 index 0000000000..52563d79eb --- /dev/null +++ b/ui-v2/app/components/modal-dialog.js @@ -0,0 +1,119 @@ +import { get, set } from '@ember/object'; +import { inject as service } from '@ember/service'; +import Component from 'consul-ui/components/dom-buffer'; +import SlotsMixin from 'ember-block-slots'; +import WithResizing from 'consul-ui/mixins/with-resizing'; + +import templatize from 'consul-ui/utils/templatize'; +export default Component.extend(SlotsMixin, WithResizing, { + dom: service('dom'), + checked: true, + height: null, + // dialog is a reference to the modal-dialog 'panel' so its 'window' + dialog: null, + overflowingClass: 'overflowing', + onclose: function() {}, + onopen: function() {}, + _open: function(e) { + set(this, 'checked', true); + if (get(this, 'height') === null) { + if (this.element) { + const dialogPanel = get(this, 'dom').element('[role="dialog"] > div > div', this.element); + const rect = dialogPanel.getBoundingClientRect(); + set(this, 'dialog', dialogPanel); + set(this, 'height', rect.height); + } + } + this.didAppear(); + this.onopen(e); + }, + didAppear: function() { + this._super(...arguments); + if (get(this, 'checked')) { + get(this, 'dom') + .root() + .classList.add(...templatize(['with-modal'])); + } + }, + _close: function(e) { + set(this, 'checked', false); + const dialogPanel = get(this, 'dialog'); + const overflowing = get(this, 'overflowingClass'); + if (dialogPanel.classList.contains(overflowing)) { + dialogPanel.classList.remove(overflowing); + } + // TODO: should we make a didDisappear? + get(this, 'dom') + .root() + .classList.remove(...templatize(['with-modal'])); + this.onclose(e); + }, + didReceiveAttrs: function() { + this._super(...arguments); + // TODO: Why does setting name mean checked it false? + // It's because if it has a name then it is likely to be linked + // to HTML state rather than just being added via HTMLBars + // and therefore likely to be immediately on the page + // It's not our usecase just yet, but this should check the state + // of the thing its linked to, incase that has a `checked` of true + // right now we know ours is always false. + if (get(this, 'name')) { + set(this, 'checked', false); + } + if (this.element) { + if (get(this, 'checked')) { + // TODO: probably need an event here + // possibly this.element for the target + // or find the input + this._open({ target: {} }); + } + } + }, + didInsertElement: function() { + this._super(...arguments); + if (get(this, 'checked')) { + // TODO: probably need an event here + // possibly this.element for the target + // or find the input + this._open({ target: {} }); + } + }, + didDestroyElement: function() { + this._super(...arguments); + get(this, 'dom') + .root() + .classList.remove(...templatize(['with-modal'])); + }, + resize: function(e) { + if (get(this, 'checked')) { + const height = get(this, 'height'); + if (height !== null) { + const dialogPanel = get(this, 'dialog'); + const overflowing = get(this, 'overflowingClass'); + if (height > e.detail.height) { + if (!dialogPanel.classList.contains(overflowing)) { + dialogPanel.classList.add(overflowing); + } + return; + } else { + if (dialogPanel.classList.contains(overflowing)) { + dialogPanel.classList.remove(overflowing); + } + } + } + } + }, + actions: { + change: function(e) { + if (e && e.target && e.target.checked) { + this._open(e); + } else { + this._close(); + } + }, + close: function() { + get(this, 'dom').element('#modal_close').checked = true; + this.onclose(); + }, + }, +}); diff --git a/ui-v2/app/components/modal-layer.js b/ui-v2/app/components/modal-layer.js new file mode 100644 index 0000000000..66e760e563 --- /dev/null +++ b/ui-v2/app/components/modal-layer.js @@ -0,0 +1,18 @@ +import Component from 'consul-ui/components/dom-buffer-flush'; +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; + +export default Component.extend({ + dom: service('dom'), + actions: { + change: function(e) { + [...get(this, 'dom').elements('[name="modal"]')] + .filter(function(item) { + return item.getAttribute('id') !== 'modal_close'; + }) + .forEach(function(item) { + item.onchange(); + }); + }, + }, +}); diff --git a/ui-v2/app/components/secret-button.js b/ui-v2/app/components/secret-button.js new file mode 100644 index 0000000000..5570647734 --- /dev/null +++ b/ui-v2/app/components/secret-button.js @@ -0,0 +1,3 @@ +import Component from '@ember/component'; + +export default Component.extend({}); diff --git a/ui-v2/app/components/tabular-collection.js b/ui-v2/app/components/tabular-collection.js index 5c297cb075..ab4e8db65c 100644 --- a/ui-v2/app/components/tabular-collection.js +++ b/ui-v2/app/components/tabular-collection.js @@ -5,7 +5,11 @@ import Grid from 'ember-collection/layouts/grid'; import SlotsMixin from 'ember-block-slots'; import WithResizing from 'consul-ui/mixins/with-resizing'; import style from 'ember-computed-style'; -import qsaFactory from 'consul-ui/utils/qsa-factory'; +import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; +import sibling from 'consul-ui/utils/dom/sibling'; +import closest from 'consul-ui/utils/dom/closest'; +import clickFirstAnchorFactory from 'consul-ui/utils/dom/click-first-anchor'; +const clickFirstAnchor = clickFirstAnchorFactory(closest); import { computed, get, set } from '@ember/object'; /** @@ -53,26 +57,6 @@ class ZIndexedGrid extends Grid { return style; } } -// basic DOM closest utility to cope with no support -// TODO: instead of degrading gracefully -// add a while polyfill for closest -const closest = function(sel, el) { - try { - return el.closest(sel); - } catch (e) { - return; - } -}; -const sibling = function(el, name) { - let sibling = el; - while ((sibling = sibling.nextSibling)) { - if (sibling.nodeType === 1) { - if (sibling.nodeName.toLowerCase() === name) { - return sibling; - } - } - } -}; /** * The tabular-collection can contain 'actions' the UI for which * uses dropdown 'action groups', so a group of different actions. @@ -131,11 +115,13 @@ const change = function(e) { }; export default Component.extend(SlotsMixin, WithResizing, { tagName: 'table', + classNames: ['dom-recycling'], attributeBindings: ['style'], width: 1150, height: 500, style: style('getStyle'), checked: null, + hasCaption: false, init: function() { this._super(...arguments); this.change = change.bind(this); @@ -149,12 +135,13 @@ export default Component.extend(SlotsMixin, WithResizing, { }; }), resize: function(e) { - const $tbody = [...$$('tbody', this.element)][0]; + const $tbody = this.element; const $appContent = [...$$('main > div')][0]; if ($appContent) { + const border = 1; const rect = $tbody.getBoundingClientRect(); const $footer = [...$$('footer[role="contentinfo"]')][0]; - const space = rect.top + $footer.clientHeight; + const space = rect.top + $footer.clientHeight + border; const height = e.detail.height - space; this.set('height', Math.max(0, height)); // TODO: The row height should auto calculate properly from the CSS @@ -165,7 +152,8 @@ export default Component.extend(SlotsMixin, WithResizing, { }, willRender: function() { this._super(...arguments); - this.set('hasActions', this._isRegistered('actions')); + set(this, 'hasCaption', this._isRegistered('caption')); + set(this, 'hasActions', this._isRegistered('actions')); }, // `ember-collection` bug workaround // https://github.com/emberjs/ember-collection/issues/138 @@ -285,26 +273,7 @@ export default Component.extend(SlotsMixin, WithResizing, { }, actions: { click: function(e) { - // click on row functionality - // so if you click the actual row but not a link - // find the first link and fire that instead - const name = e.target.nodeName.toLowerCase(); - switch (name) { - case 'input': - case 'label': - case 'a': - case 'button': - return; - } - const $a = closest('tr', e.target).querySelector('a'); - if ($a) { - const click = new MouseEvent('click', { - bubbles: true, - cancelable: true, - view: window, - }); - $a.dispatchEvent(click); - } + return clickFirstAnchor(e); }, }, }); diff --git a/ui-v2/app/components/tabular-details.js b/ui-v2/app/components/tabular-details.js new file mode 100644 index 0000000000..9cb8f8360b --- /dev/null +++ b/ui-v2/app/components/tabular-details.js @@ -0,0 +1,17 @@ +import Component from '@ember/component'; +import SlotsMixin from 'ember-block-slots'; +import closest from 'consul-ui/utils/dom/closest'; +import clickFirstAnchorFactory from 'consul-ui/utils/dom/click-first-anchor'; +const clickFirstAnchor = clickFirstAnchorFactory(closest); + +export default Component.extend(SlotsMixin, { + onchange: function() {}, + actions: { + click: function(e) { + clickFirstAnchor(e); + }, + change: function(item, e) { + this.onchange(e, item); + }, + }, +}); diff --git a/ui-v2/app/components/token-list.js b/ui-v2/app/components/token-list.js new file mode 100644 index 0000000000..e0fd578e28 --- /dev/null +++ b/ui-v2/app/components/token-list.js @@ -0,0 +1,6 @@ +import Component from '@ember/component'; +import SlotsMixin from 'ember-block-slots'; + +export default Component.extend(SlotsMixin, { + tagName: '', +}); diff --git a/ui-v2/app/controllers/dc/acls/policies/create.js b/ui-v2/app/controllers/dc/acls/policies/create.js new file mode 100644 index 0000000000..4723e0ce43 --- /dev/null +++ b/ui-v2/app/controllers/dc/acls/policies/create.js @@ -0,0 +1,2 @@ +import Controller from './edit'; +export default Controller.extend(); diff --git a/ui-v2/app/controllers/dc/acls/policies/edit.js b/ui-v2/app/controllers/dc/acls/policies/edit.js new file mode 100644 index 0000000000..1c02cbfaf1 --- /dev/null +++ b/ui-v2/app/controllers/dc/acls/policies/edit.js @@ -0,0 +1,45 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { get, set } from '@ember/object'; +export default Controller.extend({ + builder: service('form'), + dom: service('dom'), + isScoped: false, + init: function() { + this._super(...arguments); + this.form = get(this, 'builder').form('policy'); + }, + setProperties: function(model) { + // essentially this replaces the data with changesets + this._super( + Object.keys(model).reduce((prev, key, i) => { + switch (key) { + case 'item': + prev[key] = this.form.setData(prev[key]).getData(); + break; + } + return prev; + }, model) + ); + set(this, 'isScoped', get(model.item, 'Datacenters.length') > 0); + }, + actions: { + change: function(e, value, item) { + const form = get(this, 'form'); + const event = get(this, 'dom').normalizeEvent(e, value); + try { + form.handleEvent(event); + } catch (err) { + const target = event.target; + switch (target.name) { + case 'policy[isScoped]': + set(this, 'isScoped', !get(this, 'isScoped')); + set(this.item, 'Datacenters', null); + break; + default: + throw err; + } + } + }, + }, +}); diff --git a/ui-v2/app/controllers/dc/acls/policies/index.js b/ui-v2/app/controllers/dc/acls/policies/index.js new file mode 100644 index 0000000000..534a82cda2 --- /dev/null +++ b/ui-v2/app/controllers/dc/acls/policies/index.js @@ -0,0 +1,23 @@ +import Controller from '@ember/controller'; +import { get } from '@ember/object'; +import WithFiltering from 'consul-ui/mixins/with-filtering'; +export default Controller.extend(WithFiltering, { + queryParams: { + s: { + as: 'filter', + replace: true, + }, + }, + filter: function(item, { s = '', type = '' }) { + const sLower = s.toLowerCase(); + return ( + get(item, 'Name') + .toLowerCase() + .indexOf(sLower) !== -1 || + get(item, 'Description') + .toLowerCase() + .indexOf(sLower) !== -1 + ); + }, + actions: {}, +}); diff --git a/ui-v2/app/controllers/dc/acls/tokens/create.js b/ui-v2/app/controllers/dc/acls/tokens/create.js new file mode 100644 index 0000000000..4723e0ce43 --- /dev/null +++ b/ui-v2/app/controllers/dc/acls/tokens/create.js @@ -0,0 +1,2 @@ +import Controller from './edit'; +export default Controller.extend(); diff --git a/ui-v2/app/controllers/dc/acls/tokens/edit.js b/ui-v2/app/controllers/dc/acls/tokens/edit.js new file mode 100644 index 0000000000..a10c6e10e6 --- /dev/null +++ b/ui-v2/app/controllers/dc/acls/tokens/edit.js @@ -0,0 +1,79 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { get, set } from '@ember/object'; +export default Controller.extend({ + dom: service('dom'), + builder: service('form'), + isScoped: false, + init: function() { + this._super(...arguments); + this.form = get(this, 'builder').form('token'); + }, + setProperties: function(model) { + // essentially this replaces the data with changesets + this._super( + Object.keys(model).reduce((prev, key, i) => { + switch (key) { + case 'item': + prev[key] = this.form.setData(prev[key]).getData(); + break; + case 'policy': + prev[key] = this.form + .form(key) + .setData(prev[key]) + .getData(); + break; + } + return prev; + }, model) + ); + }, + actions: { + sendClearPolicy: function(item) { + set(this, 'isScoped', false); + this.send('clearPolicy'); + }, + sendCreatePolicy: function(item, policies, success) { + this.send('createPolicy', item, policies, success); + }, + refreshCodeEditor: function(selector, parent) { + if (parent.target) { + parent = undefined; + } + get(this, 'dom') + .component(selector, parent) + .didAppear(); + }, + change: function(e, value, item) { + const form = get(this, 'form'); + const event = get(this, 'dom').normalizeEvent(e, value); + try { + form.handleEvent(event); + } catch (err) { + const target = event.target; + switch (target.name) { + case 'policy[isScoped]': + set(this, 'isScoped', !get(this, 'isScoped')); + set(this.policy, 'Datacenters', null); + break; + case 'Policy': + set(value, 'CreateTime', new Date().getTime()); + get(this, 'item.Policies').pushObject(value); + break; + case 'Details': + // the Details expander toggle + // only load on opening + if (target.checked) { + this.send('refreshCodeEditor', '.code-editor', target.parentNode); + if (!get(value, 'Rules')) { + this.send('loadPolicy', value, get(this, 'item.Policies')); + } + } + break; + default: + throw err; + } + } + }, + }, +}); diff --git a/ui-v2/app/controllers/dc/acls/tokens/index.js b/ui-v2/app/controllers/dc/acls/tokens/index.js new file mode 100644 index 0000000000..729664c72c --- /dev/null +++ b/ui-v2/app/controllers/dc/acls/tokens/index.js @@ -0,0 +1,33 @@ +import Controller from '@ember/controller'; +import { get } from '@ember/object'; +import WithFiltering from 'consul-ui/mixins/with-filtering'; +export default Controller.extend(WithFiltering, { + queryParams: { + s: { + as: 'filter', + replace: true, + }, + }, + filter: function(item, { s = '', type = '' }) { + const sLower = s.toLowerCase(); + return ( + get(item, 'AccessorID') + .toLowerCase() + .indexOf(sLower) !== -1 || + get(item, 'Name') + .toLowerCase() + .indexOf(sLower) !== -1 || + get(item, 'Description') + .toLowerCase() + .indexOf(sLower) !== -1 || + (get(item, 'Policies') || []).some(function(item) { + return item.Name.toLowerCase().indexOf(sLower) !== -1; + }) + ); + }, + actions: { + sendClone: function(item) { + this.send('clone', item); + }, + }, +}); diff --git a/ui-v2/app/controllers/dc/nodes/show.js b/ui-v2/app/controllers/dc/nodes/show.js index b93c00f00d..1eb34e6bec 100644 --- a/ui-v2/app/controllers/dc/nodes/show.js +++ b/ui-v2/app/controllers/dc/nodes/show.js @@ -2,7 +2,7 @@ import Controller from '@ember/controller'; import { get, set } from '@ember/object'; import { getOwner } from '@ember/application'; import WithFiltering from 'consul-ui/mixins/with-filtering'; -import qsaFactory from 'consul-ui/utils/qsa-factory'; +import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; import getComponentFactory from 'consul-ui/utils/get-component-factory'; const $$ = qsaFactory(); diff --git a/ui-v2/app/forms/policy.js b/ui-v2/app/forms/policy.js new file mode 100644 index 0000000000..f1452fd97b --- /dev/null +++ b/ui-v2/app/forms/policy.js @@ -0,0 +1,10 @@ +import validations from 'consul-ui/validations/policy'; +import builderFactory from 'consul-ui/utils/form/builder'; +const builder = builderFactory(); +export default function(name = 'policy', v = validations, form = builder) { + return form(name, { + Datacenters: { + type: 'array', + }, + }).setValidators(v); +} diff --git a/ui-v2/app/forms/token.js b/ui-v2/app/forms/token.js new file mode 100644 index 0000000000..3a0e0bb82e --- /dev/null +++ b/ui-v2/app/forms/token.js @@ -0,0 +1,9 @@ +import builderFactory from 'consul-ui/utils/form/builder'; +import validations from 'consul-ui/validations/token'; +import policy from 'consul-ui/forms/policy'; +const builder = builderFactory(); +export default function(name = '', v = validations, form = builder) { + return form(name, {}) + .setValidators(v) + .add(policy()); +} diff --git a/ui-v2/app/helpers/difference.js b/ui-v2/app/helpers/difference.js new file mode 100644 index 0000000000..983b4a4006 --- /dev/null +++ b/ui-v2/app/helpers/difference.js @@ -0,0 +1,9 @@ +import { helper } from '@ember/component/helper'; +import { get } from '@ember/object'; +export function difference(params, hash) { + return params[0].filter(function(item) { + return !params[1].findBy('ID', get(item, 'ID')); + }); +} + +export default helper(difference); diff --git a/ui-v2/app/helpers/policy/datacenters.js b/ui-v2/app/helpers/policy/datacenters.js new file mode 100644 index 0000000000..f9e149870f --- /dev/null +++ b/ui-v2/app/helpers/policy/datacenters.js @@ -0,0 +1,16 @@ +import { helper } from '@ember/component/helper'; +import { get } from '@ember/object'; + +/** + * Datacenters can be an array of datacenters. + * Anything that isn't an array means 'All', even an empty array. + */ +export function datacenters(params, hash = {}) { + const datacenters = get(params[0], 'Datacenters'); + if (!Array.isArray(datacenters) || datacenters.length === 0) { + return [hash['global'] || 'All']; + } + return get(params[0], 'Datacenters'); +} + +export default helper(datacenters); diff --git a/ui-v2/app/helpers/policy/is-management.js b/ui-v2/app/helpers/policy/is-management.js new file mode 100644 index 0000000000..d35093f227 --- /dev/null +++ b/ui-v2/app/helpers/policy/is-management.js @@ -0,0 +1,8 @@ +import { helper } from '@ember/component/helper'; +import { get } from '@ember/object'; +const MANAGEMENT_ID = '00000000-0000-0000-0000-000000000001'; +export function isManagement(params, hash) { + return get(params[0], 'ID') === MANAGEMENT_ID; +} + +export default helper(isManagement); diff --git a/ui-v2/app/helpers/token/is-anonymous.js b/ui-v2/app/helpers/token/is-anonymous.js new file mode 100644 index 0000000000..ffa8329a4c --- /dev/null +++ b/ui-v2/app/helpers/token/is-anonymous.js @@ -0,0 +1,8 @@ +import { helper } from '@ember/component/helper'; +import { get } from '@ember/object'; + +const ANONYMOUS_ID = '00000000-0000-0000-0000-000000000002'; +export function isAnonymous(params, hash) { + return get(params[0], 'AccessorID') === ANONYMOUS_ID; +} +export default helper(isAnonymous); diff --git a/ui-v2/app/helpers/token/is-legacy.js b/ui-v2/app/helpers/token/is-legacy.js new file mode 100644 index 0000000000..215b380b2e --- /dev/null +++ b/ui-v2/app/helpers/token/is-legacy.js @@ -0,0 +1,9 @@ +import { helper } from '@ember/component/helper'; +import { get } from '@ember/object'; + +export function isLegacy(params, hash) { + const token = params[0]; + return get(token, 'Legacy') || typeof get(token, 'Rules') !== 'undefined'; +} + +export default helper(isLegacy); diff --git a/ui-v2/app/initializers/form.js b/ui-v2/app/initializers/form.js new file mode 100644 index 0000000000..099e98036c --- /dev/null +++ b/ui-v2/app/initializers/form.js @@ -0,0 +1,19 @@ +import token from 'consul-ui/forms/token'; +import policy from 'consul-ui/forms/policy'; +export function initialize(application) { + // Service-less injection using private properties at a per-project level + const FormBuilder = application.resolveRegistration('service:form'); + const forms = { + token: token(), + policy: policy(), + }; + FormBuilder.reopen({ + form: function(name) { + return forms[name]; + }, + }); +} + +export default { + initialize, +}; diff --git a/ui-v2/app/initializers/ivy-codemirror.js b/ui-v2/app/initializers/ivy-codemirror.js new file mode 100644 index 0000000000..c4f3436057 --- /dev/null +++ b/ui-v2/app/initializers/ivy-codemirror.js @@ -0,0 +1,11 @@ +export function initialize(application) { + const IvyCodeMirrorComponent = application.resolveRegistration('component:ivy-codemirror'); + // Make sure ivy-codemirror respects/maintains a `name=""` attribute + IvyCodeMirrorComponent.reopen({ + attributeBindings: ['name'], + }); +} + +export default { + initialize, +}; diff --git a/ui-v2/app/mixins/creating-route.js b/ui-v2/app/mixins/creating-route.js new file mode 100644 index 0000000000..6ee9f0e4bc --- /dev/null +++ b/ui-v2/app/mixins/creating-route.js @@ -0,0 +1,24 @@ +import Mixin from '@ember/object/mixin'; +import { get } from '@ember/object'; + +/** + * Used for create-type Routes + * + * 'repo' is standardized across the app + * 'item' is standardized across the app + * they could be replaced with `getRepo` and `getItem` + */ +export default Mixin.create({ + beforeModel: function() { + get(this, 'repo').invalidate(); + }, + deactivate: function() { + // TODO: This is dependent on ember-changeset + // Change changeset to support ember-data props + const item = get(this.controller, 'item.data'); + // TODO: Look and see if rollbackAttributes is good here + if (get(item, 'isNew')) { + item.destroyRecord(); + } + }, +}); diff --git a/ui-v2/app/mixins/policy/with-actions.js b/ui-v2/app/mixins/policy/with-actions.js new file mode 100644 index 0000000000..fc75a41913 --- /dev/null +++ b/ui-v2/app/mixins/policy/with-actions.js @@ -0,0 +1,4 @@ +import Mixin from '@ember/object/mixin'; +import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; + +export default Mixin.create(WithBlockingActions, {}); diff --git a/ui-v2/app/mixins/token/with-actions.js b/ui-v2/app/mixins/token/with-actions.js new file mode 100644 index 0000000000..78db873c4f --- /dev/null +++ b/ui-v2/app/mixins/token/with-actions.js @@ -0,0 +1,61 @@ +import Mixin from '@ember/object/mixin'; +import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; +import { get } from '@ember/object'; +import { inject as service } from '@ember/service'; + +export default Mixin.create(WithBlockingActions, { + settings: service('settings'), + actions: { + use: function(item) { + return get(this, 'feedback').execute(() => { + return get(this, 'repo') + .findBySlug(get(item, 'AccessorID'), this.modelFor('dc').dc.Name) + .then(item => { + return get(this, 'settings') + .persist({ + token: { + AccessorID: get(item, 'AccessorID'), + SecretID: get(item, 'SecretID'), + }, + }) + .then(() => { + // using is similar to delete in that + // if you use from the listing page, stay on the listing page + // whereas if you use from the detail page, take me back to the listing page + return this.afterDelete(...arguments); + }); + }); + }, 'use'); + }, + logout: function(item) { + return get(this, 'feedback').execute(() => { + return get(this, 'settings') + .delete('token') + .then(() => { + // logging out is similar to delete in that + // if you log out from the listing page, stay on the listing page + // whereas if you logout from the detail page, take me back to the listing page + return this.afterDelete(...arguments); + }); + }, 'logout'); + }, + clone: function(item) { + let cloned; + return get(this, 'feedback').execute(() => { + return get(this, 'repo') + .clone(item) + .then(item => { + cloned = item; + // cloning is similar to delete in that + // if you clone from the listing page, stay on the listing page + // whereas if you clone from another token, take me back to the listing page + // so I can see it + return this.afterDelete(...arguments); + }) + .then(function() { + return cloned; + }); + }, 'clone'); + }, + }, +}); diff --git a/ui-v2/app/mixins/with-blocking-actions.js b/ui-v2/app/mixins/with-blocking-actions.js index 5cfca5d0ef..97ff9fb5a2 100644 --- a/ui-v2/app/mixins/with-blocking-actions.js +++ b/ui-v2/app/mixins/with-blocking-actions.js @@ -83,7 +83,7 @@ export default Mixin.create({ } ); }, - update: function(item, parent) { + update: function(item) { return get(this, 'feedback').execute( () => { return get(this, 'repo') @@ -98,7 +98,7 @@ export default Mixin.create({ } ); }, - delete: function(item, parent) { + delete: function(item) { return get(this, 'feedback').execute( () => { return get(this, 'repo') diff --git a/ui-v2/app/models/policy.js b/ui-v2/app/models/policy.js new file mode 100644 index 0000000000..0bbf498278 --- /dev/null +++ b/ui-v2/app/models/policy.js @@ -0,0 +1,29 @@ +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import writable from 'consul-ui/utils/model/writable'; + +export const PRIMARY_KEY = 'uid'; +export const SLUG_KEY = 'ID'; + +const model = Model.extend({ + [PRIMARY_KEY]: attr('string'), + [SLUG_KEY]: attr('string'), + Name: attr('string', { + defaultValue: '', + }), + Description: attr('string', { + defaultValue: '', + }), + Rules: attr('string', { + defaultValue: '', + }), + // frontend only for ordering where CreateIndex can't be used + CreateTime: attr('date'), + // + Datacenter: attr('string'), + Datacenters: attr(), + CreateIndex: attr('number'), + ModifyIndex: attr('number'), +}); +export const ATTRS = writable(model, ['Name', 'Description', 'Rules', 'Datacenters']); +export default model; diff --git a/ui-v2/app/models/service.js b/ui-v2/app/models/service.js index c8e71ae05d..cf98df3114 100644 --- a/ui-v2/app/models/service.js +++ b/ui-v2/app/models/service.js @@ -21,6 +21,7 @@ export default Model.extend({ EnableTagOverride: attr('boolean'), CreateIndex: attr('number'), ModifyIndex: attr('number'), + // TODO: These should be typed ChecksPassing: attr(), ChecksCritical: attr(), ChecksWarning: attr(), diff --git a/ui-v2/app/models/token.js b/ui-v2/app/models/token.js new file mode 100644 index 0000000000..48e2b469e7 --- /dev/null +++ b/ui-v2/app/models/token.js @@ -0,0 +1,47 @@ +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import writable from 'consul-ui/utils/model/writable'; + +export const PRIMARY_KEY = 'uid'; +export const SLUG_KEY = 'AccessorID'; + +const model = Model.extend({ + [PRIMARY_KEY]: attr('string'), + [SLUG_KEY]: attr('string'), + SecretID: attr('string'), + // Legacy + Type: attr('string'), + Name: attr('string', { + defaultValue: '', + }), + Rules: attr('string'), + // End Legacy + Legacy: attr('boolean'), + Description: attr('string', { + defaultValue: '', + }), + Datacenter: attr('string'), + Local: attr('boolean'), + Policies: attr({ + defaultValue: function() { + return []; + }, + }), + CreateTime: attr('date'), + CreateIndex: attr('number'), + ModifyIndex: attr('number'), +}); +// Name and Rules is only for legacy tokens +export const ATTRS = writable(model, [ + 'Name', + 'Rules', + 'Type', + 'Local', + 'Description', + 'Policies', + // SecretID isn't writable but we need it to identify an + // update via the old API, see TokenAdapter dataForRequest + 'SecretID', + 'AccessorID', +]); +export default model; diff --git a/ui-v2/app/router.js b/ui-v2/app/router.js index 01f8bf0dfc..f240ed1f69 100644 --- a/ui-v2/app/router.js +++ b/ui-v2/app/router.js @@ -35,6 +35,14 @@ Router.map(function() { this.route('acls', { path: '/acls' }, function() { this.route('edit', { path: '/:id' }); this.route('create', { path: '/create' }); + this.route('policies', { path: '/policies' }, function() { + this.route('edit', { path: '/:id' }); + this.route('create', { path: '/create' }); + }); + this.route('tokens', { path: '/tokens' }, function() { + this.route('edit', { path: '/:id' }); + this.route('create', { path: '/create' }); + }); }); }); @@ -43,7 +51,7 @@ Router.map(function() { this.route('index', { path: '/' }); // The settings page is global. - this.route('settings', { path: '/settings' }); + // this.route('settings', { path: '/settings' }); this.route('notfound', { path: '/*path' }); }); diff --git a/ui-v2/app/routes/application.js b/ui-v2/app/routes/application.js index 87e0ee1ea6..2f17c18c4f 100644 --- a/ui-v2/app/routes/application.js +++ b/ui-v2/app/routes/application.js @@ -38,6 +38,7 @@ export default Route.extend({ return true; }, error: function(e, transition) { + // TODO: Normalize all this better let error = { status: e.code || '', message: e.message || e.detail || 'Error', @@ -46,6 +47,20 @@ export default Route.extend({ error = e.errors[0]; error.message = error.title || error.detail || 'Error'; } + // TODO: Unfortunately ember will not maintain the correct URL + // for you i.e. when this happens the URL in your browser location bar + // will be the URL where you clicked on the link to come here + // not the URL where you got the 403 response + // Currently this is dealt with a lot better with the new ACLs system, in that + // if you get a 403 in the ACLs area, the URL is correct + // Moving that app wide right now wouldn't be ideal, therefore simply redirect + // to the ACLs URL instead of maintaining the actual URL, which is better than the old + // 403 page + // To note: Consul only gives you back a 403 if a non-existent token has been sent in the header + // if a token has not been sent at all, it just gives you a 200 with an empty dataset + if (error.status === '403') { + return this.transitionTo('dc.acls.tokens'); + } if (error.status === '') { error.message = 'Error'; } diff --git a/ui-v2/app/routes/dc/acls.js b/ui-v2/app/routes/dc/acls.js new file mode 100644 index 0000000000..b7baccaf1a --- /dev/null +++ b/ui-v2/app/routes/dc/acls.js @@ -0,0 +1,31 @@ +import Route from '@ember/routing/route'; +import { get } from '@ember/object'; +import { inject as service } from '@ember/service'; +import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; + +export default Route.extend(WithBlockingActions, { + settings: service('settings'), + feedback: service('feedback'), + repo: service('tokens'), + actions: { + authorize: function(secret) { + const dc = this.modelFor('dc').dc.Name; + return get(this, 'feedback').execute(() => { + return get(this, 'repo') + .self(secret, dc) + .then(item => { + get(this, 'settings') + .persist({ + token: { + AccessorID: get(item, 'AccessorID'), + SecretID: secret, + }, + }) + .then(() => { + this.refresh(); + }); + }); + }, 'authorize'); + }, + }, +}); diff --git a/ui-v2/app/routes/dc/acls/index.js b/ui-v2/app/routes/dc/acls/index.js index 37e7589885..6e7804d6b2 100644 --- a/ui-v2/app/routes/dc/acls/index.js +++ b/ui-v2/app/routes/dc/acls/index.js @@ -13,6 +13,9 @@ export default Route.extend(WithAclActions, { replace: true, }, }, + beforeModel: function(transition) { + return this.replaceWith('dc.acls.tokens'); + }, model: function(params) { return hash({ isLoading: false, diff --git a/ui-v2/app/routes/dc/acls/policies/create.js b/ui-v2/app/routes/dc/acls/policies/create.js new file mode 100644 index 0000000000..21781c92a7 --- /dev/null +++ b/ui-v2/app/routes/dc/acls/policies/create.js @@ -0,0 +1,6 @@ +import Route from './edit'; +import CreatingRoute from 'consul-ui/mixins/creating-route'; + +export default Route.extend(CreatingRoute, { + templateName: 'dc/acls/policies/edit', +}); diff --git a/ui-v2/app/routes/dc/acls/policies/edit.js b/ui-v2/app/routes/dc/acls/policies/edit.js new file mode 100644 index 0000000000..22f8e09f4b --- /dev/null +++ b/ui-v2/app/routes/dc/acls/policies/edit.js @@ -0,0 +1,37 @@ +import SingleRoute from 'consul-ui/routing/single'; +import { inject as service } from '@ember/service'; +import { hash } from 'rsvp'; +import { get } from '@ember/object'; + +import WithPolicyActions from 'consul-ui/mixins/policy/with-actions'; + +export default SingleRoute.extend(WithPolicyActions, { + repo: service('policies'), + tokensRepo: service('tokens'), + datacenterRepo: service('dc'), + model: function(params) { + const dc = this.modelFor('dc').dc.Name; + const tokensRepo = get(this, 'tokensRepo'); + return this._super(...arguments).then(model => { + return hash({ + ...model, + ...{ + datacenters: get(this, 'datacenterRepo').findAll(), + items: tokensRepo.findByPolicy(get(model.item, 'ID'), dc).catch(function(e) { + switch (get(e, 'errors.firstObject.status')) { + case '403': + case '401': + // do nothing the SingleRoute will have caught it already + return; + } + throw e; + }), + }, + }); + }); + }, + setupController: function(controller, model) { + this._super(...arguments); + controller.setProperties(model); + }, +}); diff --git a/ui-v2/app/routes/dc/acls/policies/index.js b/ui-v2/app/routes/dc/acls/policies/index.js new file mode 100644 index 0000000000..adfc273c4a --- /dev/null +++ b/ui-v2/app/routes/dc/acls/policies/index.js @@ -0,0 +1,29 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { hash } from 'rsvp'; +import { get } from '@ember/object'; + +import WithPolicyActions from 'consul-ui/mixins/policy/with-actions'; + +export default Route.extend(WithPolicyActions, { + repo: service('policies'), + queryParams: { + s: { + as: 'filter', + replace: true, + }, + }, + model: function(params) { + const repo = get(this, 'repo'); + return hash({ + ...repo.status({ + items: repo.findAllByDatacenter(this.modelFor('dc').dc.Name), + }), + isLoading: false, + }); + }, + setupController: function(controller, model) { + this._super(...arguments); + controller.setProperties(model); + }, +}); diff --git a/ui-v2/app/routes/dc/acls/tokens/create.js b/ui-v2/app/routes/dc/acls/tokens/create.js new file mode 100644 index 0000000000..3e76e6b2cf --- /dev/null +++ b/ui-v2/app/routes/dc/acls/tokens/create.js @@ -0,0 +1,6 @@ +import Route from './edit'; +import CreatingRoute from 'consul-ui/mixins/creating-route'; + +export default Route.extend(CreatingRoute, { + templateName: 'dc/acls/tokens/edit', +}); diff --git a/ui-v2/app/routes/dc/acls/tokens/edit.js b/ui-v2/app/routes/dc/acls/tokens/edit.js new file mode 100644 index 0000000000..31dacdff1e --- /dev/null +++ b/ui-v2/app/routes/dc/acls/tokens/edit.js @@ -0,0 +1,105 @@ +import SingleRoute from 'consul-ui/routing/single'; +import { inject as service } from '@ember/service'; +import { hash } from 'rsvp'; +import { set, get } from '@ember/object'; +import updateArrayObject from 'consul-ui/utils/update-array-object'; + +import WithTokenActions from 'consul-ui/mixins/token/with-actions'; + +const ERROR_PARSE_RULES = 'Failed to parse ACL rules'; +const ERROR_NAME_EXISTS = 'Invalid Policy: A Policy with Name'; +export default SingleRoute.extend(WithTokenActions, { + repo: service('tokens'), + policiesRepo: service('policies'), + datacenterRepo: service('dc'), + settings: service('settings'), + model: function(params, transition) { + const dc = this.modelFor('dc').dc.Name; + const policiesRepo = get(this, 'policiesRepo'); + return this._super(...arguments).then(model => { + return hash({ + ...model, + ...{ + // TODO: I only need these to create a new policy + datacenters: get(this, 'datacenterRepo').findAll(), + policy: this.getEmptyPolicy(), + token: get(this, 'settings').findBySlug('token'), + items: policiesRepo.findAllByDatacenter(dc).catch(function(e) { + switch (get(e, 'errors.firstObject.status')) { + case '403': + case '401': + // do nothing the SingleRoute will have caught it already + return; + } + throw e; + }), + }, + }); + }); + }, + setupController: function(controller, model) { + this._super(...arguments); + controller.setProperties(model); + }, + getEmptyPolicy: function() { + const dc = this.modelFor('dc').dc.Name; + return get(this, 'policiesRepo').create({ Datacenter: dc }); + }, + actions: { + // TODO: Some of this could potentially be moved to the repo services + loadPolicy: function(item, items) { + const repo = get(this, 'policiesRepo'); + const dc = this.modelFor('dc').dc.Name; + const slug = get(item, repo.getSlugKey()); + repo.findBySlug(slug, dc).then(item => { + updateArrayObject(items, item, repo.getSlugKey()); + }); + }, + remove: function(item, items) { + return items.removeObject(item); + }, + clearPolicy: function() { + // TODO: I should be able to reset the ember-data object + // back to it original state? + // possibly Forms could know how to create + const controller = get(this, 'controller'); + controller.setProperties({ + policy: this.getEmptyPolicy(), + }); + }, + createPolicy: function(item, policies, success) { + get(this, 'policiesRepo') + .persist(item) + .then(item => { + set(item, 'CreateTime', new Date().getTime()); + policies.pushObject(item); + return item; + }) + .then(function() { + success(); + }) + .catch(err => { + if (typeof err.errors !== 'undefined') { + const error = err.errors[0]; + let prop; + let message = error.detail; + switch (true) { + case message.indexOf(ERROR_PARSE_RULES) === 0: + prop = 'Rules'; + message = error.detail; + break; + case message.indexOf(ERROR_NAME_EXISTS) === 0: + prop = 'Name'; + message = message.substr(ERROR_NAME_EXISTS.indexOf(':') + 1); + break; + } + if (prop) { + item.addError(prop, message); + } + } else { + throw err; + } + }); + }, + }, +}); diff --git a/ui-v2/app/routes/dc/acls/tokens/index.js b/ui-v2/app/routes/dc/acls/tokens/index.js new file mode 100644 index 0000000000..19e9054306 --- /dev/null +++ b/ui-v2/app/routes/dc/acls/tokens/index.js @@ -0,0 +1,29 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { hash } from 'rsvp'; +import { get } from '@ember/object'; +import WithTokenActions from 'consul-ui/mixins/token/with-actions'; +export default Route.extend(WithTokenActions, { + repo: service('tokens'), + settings: service('settings'), + queryParams: { + s: { + as: 'filter', + replace: true, + }, + }, + model: function(params) { + const repo = get(this, 'repo'); + return hash({ + ...repo.status({ + items: repo.findAllByDatacenter(this.modelFor('dc').dc.Name), + }), + isLoading: false, + token: get(this, 'settings').findBySlug('token'), + }); + }, + setupController: function(controller, model) { + this._super(...arguments); + controller.setProperties(model); + }, +}); diff --git a/ui-v2/app/routes/dc/kv/index.js b/ui-v2/app/routes/dc/kv/index.js index 5d32c5df6d..10ce8ae1cc 100644 --- a/ui-v2/app/routes/dc/kv/index.js +++ b/ui-v2/app/routes/dc/kv/index.js @@ -34,7 +34,13 @@ export default Route.extend(WithKvActions, { ...model, ...{ items: repo.findAllBySlug(get(model.parent, 'Key'), dc).catch(e => { - return this.transitionTo('dc.kv.index'); + const status = get(e, 'errors.firstObject.status'); + switch (status) { + case '403': + return this.transitionTo('dc.acls.tokens'); + default: + return this.transitionTo('dc.kv.index'); + } }), }, }); diff --git a/ui-v2/app/routes/settings.js b/ui-v2/app/routes/settings.js index 8372193256..e7b07115c2 100644 --- a/ui-v2/app/routes/settings.js +++ b/ui-v2/app/routes/settings.js @@ -5,8 +5,8 @@ import { get } from '@ember/object'; import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; export default Route.extend(WithBlockingActions, { - dcRepo: service('dc'), repo: service('settings'), + dcRepo: service('dc'), model: function(params) { return hash({ item: get(this, 'repo').findAll(), diff --git a/ui-v2/app/routing/single.js b/ui-v2/app/routing/single.js new file mode 100644 index 0000000000..c0ff0efc93 --- /dev/null +++ b/ui-v2/app/routing/single.js @@ -0,0 +1,28 @@ +import Route from '@ember/routing/route'; +import { get } from '@ember/object'; +import { assert } from '@ember/debug'; +import { Promise, hash } from 'rsvp'; +export default Route.extend({ + // repo: service('repositoryName'), + isCreate: function(params, transition) { + return transition.targetName.split('.').pop() === 'create'; + }, + model: function(params, transition) { + const repo = get(this, 'repo'); + assert( + "`repo` is undefined, please define RepositoryService using `repo: service('repositoryName')`", + typeof repo !== 'undefined' + ); + const dc = this.modelFor('dc').dc.Name; + const create = this.isCreate(...arguments); + return hash({ + isLoading: false, + create: create, + ...repo.status({ + item: create + ? Promise.resolve(repo.create({ Datacenter: dc })) + : repo.findBySlug(params.id, dc), + }), + }); + }, +}); diff --git a/ui-v2/app/serializers/policy.js b/ui-v2/app/serializers/policy.js new file mode 100644 index 0000000000..db49548659 --- /dev/null +++ b/ui-v2/app/serializers/policy.js @@ -0,0 +1,7 @@ +import Serializer from './application'; +import { PRIMARY_KEY, ATTRS } from 'consul-ui/models/policy'; + +export default Serializer.extend({ + primaryKey: PRIMARY_KEY, + attrs: ATTRS, +}); diff --git a/ui-v2/app/serializers/token.js b/ui-v2/app/serializers/token.js new file mode 100644 index 0000000000..232da0d680 --- /dev/null +++ b/ui-v2/app/serializers/token.js @@ -0,0 +1,7 @@ +import Serializer from './application'; +import { PRIMARY_KEY, ATTRS } from 'consul-ui/models/token'; + +export default Serializer.extend({ + primaryKey: PRIMARY_KEY, + attrs: ATTRS, +}); diff --git a/ui-v2/app/services/dom-buffer.js b/ui-v2/app/services/dom-buffer.js new file mode 100644 index 0000000000..9cb647c4fb --- /dev/null +++ b/ui-v2/app/services/dom-buffer.js @@ -0,0 +1,21 @@ +import Service from '@ember/service'; +import Evented from '@ember/object/evented'; +const buffer = {}; +export default Service.extend(Evented, { + // TODO: Consider renaming this and/or + // `delete`ing the buffer (but not the DOM element) + // flush should flush, but maybe being able to re-flush + // after you've flushed could be handy + flush: function(name) { + return buffer[name]; + }, + add: function(name, dom) { + this.trigger('add', dom); + buffer[name] = dom; + return dom; + }, + remove: function(name) { + buffer[name].remove(); + delete buffer[name]; + }, +}); diff --git a/ui-v2/app/services/dom.js b/ui-v2/app/services/dom.js new file mode 100644 index 0000000000..4c59831da5 --- /dev/null +++ b/ui-v2/app/services/dom.js @@ -0,0 +1,54 @@ +import Service from '@ember/service'; +import { getOwner } from '@ember/application'; +import { get } from '@ember/object'; + +import qsaFactory from 'consul-ui/utils/dom/qsa-factory'; +// TODO: Move to utils/dom +import getComponentFactory from 'consul-ui/utils/get-component-factory'; +import normalizeEvent from 'consul-ui/utils/dom/normalize-event'; + +// ember-eslint doesn't like you using a single $ so use double +// use $_ for components +const $$ = qsaFactory(); +let $_; +export default Service.extend({ + doc: document, + init: function() { + this._super(...arguments); + $_ = getComponentFactory(getOwner(this)); + }, + normalizeEvent: function() { + return normalizeEvent(...arguments); + }, + root: function() { + return get(this, 'doc').documentElement; + }, + // TODO: Should I change these to use the standard names + // even though they don't have a standard signature (querySelector*) + elementById: function(id) { + return get(this, 'doc').getElementById(id); + }, + elementsByTagName: function(name, context) { + context = typeof context === 'undefined' ? get(this, 'doc') : context; + return context.getElementByTagName(name); + }, + elements: function(selector, context) { + return $$(selector, context); + }, + element: function(selector, context) { + if (selector.substr(0, 1) === '#') { + return this.elementById(selector.substr(1)); + } + // TODO: This can just use querySelector + return [...$$(selector, context)][0]; + }, + // ember components aren't strictly 'dom-like' + // but if you think of them as a web component 'shim' + // then it makes more sense to think of them as part of the dom + // with traditional/standard web components you wouldn't actually need this + // method as you could just get to their methods from the dom element + component: function(selector, context) { + // TODO: support passing a dom element, when we need to do that + return $_(this.element(selector, context)); + }, +}); diff --git a/ui-v2/app/services/feedback.js b/ui-v2/app/services/feedback.js index 2be104ef8e..ce841b7d2b 100644 --- a/ui-v2/app/services/feedback.js +++ b/ui-v2/app/services/feedback.js @@ -7,6 +7,12 @@ const TYPE_ERROR = 'error'; const defaultStatus = function(type, obj) { return type; }; +const notificationDefaults = function() { + return { + timeout: 6000, + extendedTimeout: 300, + }; +}; export default Service.extend({ notify: service('flashMessages'), logger: service('logger'), @@ -18,23 +24,29 @@ export default Service.extend({ return ( handle() //TODO: pass this through to getAction.. - .then(target => { + .then(item => { + // TODO right now the majority of `item` is a Transition + // but you can resolve an object notify.add({ + ...notificationDefaults(), type: getStatus(TYPE_SUCCESS), // here.. action: getAction(), + item: item, }); }) .catch(e => { get(this, 'logger').execute(e); if (e.name === 'TransitionAborted') { notify.add({ + ...notificationDefaults(), type: getStatus(TYPE_SUCCESS), // and here action: getAction(), }); } else { notify.add({ + ...notificationDefaults(), type: getStatus(TYPE_ERROR, e), action: getAction(), }); diff --git a/ui-v2/app/services/form.js b/ui-v2/app/services/form.js new file mode 100644 index 0000000000..933f332d9a --- /dev/null +++ b/ui-v2/app/services/form.js @@ -0,0 +1,10 @@ +import Service from '@ember/service'; +import builderFactory from 'consul-ui/utils/form/builder'; +const builder = builderFactory(); +export default Service.extend({ + // a `get` method is added via the form initializer + // see initializers/form.js + build: function(obj, name) { + return builder(...arguments); + }, +}); diff --git a/ui-v2/app/services/policies.js b/ui-v2/app/services/policies.js new file mode 100644 index 0000000000..20f0749d97 --- /dev/null +++ b/ui-v2/app/services/policies.js @@ -0,0 +1,58 @@ +import Service, { inject as service } from '@ember/service'; +import { get } from '@ember/object'; +import { typeOf } from '@ember/utils'; +import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/policy'; +import { Promise } from 'rsvp'; +import statusFactory from 'consul-ui/utils/acls-status'; +const status = statusFactory(Promise); +const MODEL_NAME = 'policy'; +export default Service.extend({ + getModelName: function() { + return MODEL_NAME; + }, + getPrimaryKey: function() { + return PRIMARY_KEY; + }, + getSlugKey: function() { + return SLUG_KEY; + }, + store: service('store'), + status: function(obj) { + return status(obj); + }, + translate: function(item) { + return get(this, 'store').translate('policy', get(item, 'Rules')); + }, + findAllByDatacenter: function(dc) { + return get(this, 'store').query('policy', { + dc: dc, + }); + }, + findBySlug: function(slug, dc) { + return get(this, 'store').queryRecord('policy', { + id: slug, + dc: dc, + }); + }, + create: function(obj) { + return get(this, 'store').createRecord('policy', obj); + }, + persist: function(item) { + return item.save(); + }, + remove: function(obj) { + let item = obj; + if (typeof obj.destroyRecord === 'undefined') { + item = obj.get('data'); + } + if (typeOf(item) === 'object') { + item = get(this, 'store').peekRecord('policy', item[PRIMARY_KEY]); + } + return item.destroyRecord().then(item => { + return get(this, 'store').unloadRecord(item); + }); + }, + invalidate: function() { + get(this, 'store').unloadAll('policy'); + }, +}); diff --git a/ui-v2/app/services/settings.js b/ui-v2/app/services/settings.js index 2e51de89a9..7d5439b5e6 100644 --- a/ui-v2/app/services/settings.js +++ b/ui-v2/app/services/settings.js @@ -1,38 +1,46 @@ import Service from '@ember/service'; import { Promise } from 'rsvp'; import { get } from '@ember/object'; - +import getStorage from 'consul-ui/utils/storage/local-storage'; +const SCHEME = 'consul'; +const storage = getStorage(SCHEME); export default Service.extend({ - // TODO: change name - storage: window.localStorage, + storage: storage, findHeaders: function() { // TODO: if possible this should be a promise - const token = get(this, 'storage').getItem('token'); + // TODO: Actually this has nothing to do with settings it should be in the adapter, + // which probably can't work with a promise based interface :( + const token = get(this, 'storage').getValue('token'); // TODO: The old UI always sent ?token= // replicate the old functionality here // but remove this to be cleaner if its not necessary return { - 'X-Consul-Token': token === null ? '' : token, + 'X-Consul-Token': typeof token.SecretID === 'undefined' ? '' : token.SecretID, }; }, findAll: function(key) { - const token = get(this, 'storage').getItem('token'); - return Promise.resolve({ token: token === null ? '' : token }); + return Promise.resolve(get(this, 'storage').all()); }, findBySlug: function(slug) { - // TODO: Force localStorage to always be strings... - // const value = get(this, 'storage').getItem(slug); - return Promise.resolve(get(this, 'storage').getItem(slug)); + return Promise.resolve(get(this, 'storage').getValue(slug)); }, persist: function(obj) { const storage = get(this, 'storage'); Object.keys(obj).forEach((item, i) => { - // TODO: ...everywhere - storage.setItem(item, obj[item]); + storage.setValue(item, obj[item]); }); return Promise.resolve(obj); }, delete: function(obj) { - return Promise.resolve(get(this, 'storage').removeItem('token')); + // TODO: Loop through and delete the specified keys + if (!Array.isArray(obj)) { + obj = [obj]; + } + const storage = get(this, 'storage'); + const item = obj.reduce(function(prev, item, i, arr) { + storage.removeValue(item); + return prev; + }, {}); + return Promise.resolve(item); }, }); diff --git a/ui-v2/app/services/store.js b/ui-v2/app/services/store.js index b5be6b849c..640e1b7d13 100644 --- a/ui-v2/app/services/store.js +++ b/ui-v2/app/services/store.js @@ -1,5 +1,7 @@ import Store from 'ember-data/store'; +// TODO: These only exist for ACLs, should probably make sure they fail +// nicely if you aren't on ACLs for good DX export default Store.extend({ // cloning immediately refreshes the view clone: function(modelName, id) { @@ -16,4 +18,9 @@ export default Store.extend({ ); // TODO: See https://github.com/emberjs/data/blob/7b8019818526a17ee72747bd3c0041354e58371a/addon/-private/system/promise-proxies.js#L68 }, + self: function(modelName, token) { + // TODO: no normalization, type it properly for the moment + const adapter = this.adapterFor(modelName); + return adapter.self(this, { modelName: modelName }, token); + }, }); diff --git a/ui-v2/app/services/tokens.js b/ui-v2/app/services/tokens.js new file mode 100644 index 0000000000..a48aeec6b1 --- /dev/null +++ b/ui-v2/app/services/tokens.js @@ -0,0 +1,72 @@ +import Service, { inject as service } from '@ember/service'; +import { get } from '@ember/object'; +import { typeOf } from '@ember/utils'; +import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/token'; +import { Promise } from 'rsvp'; +import statusFactory from 'consul-ui/utils/acls-status'; +const status = statusFactory(Promise); +const MODEL_NAME = 'token'; +export default Service.extend({ + getModelName: function() { + return MODEL_NAME; + }, + getPrimaryKey: function() { + return PRIMARY_KEY; + }, + getSlugKey: function() { + return SLUG_KEY; + }, + status: function(obj) { + return status(obj); + }, + self: function(secret, dc) { + return get(this, 'store').self(this.getModelName(), { + secret: secret, + dc: dc, + }); + }, + clone: function(item) { + return get(this, 'store').clone(this.getModelName(), get(item, PRIMARY_KEY)); + }, + findByPolicy: function(id, dc) { + return get(this, 'store').query(this.getModelName(), { + policy: id, + dc: dc, + }); + }, + // TODO: RepositoryService + store: service('store'), + findAllByDatacenter: function(dc) { + return get(this, 'store').query(this.getModelName(), { + dc: dc, + }); + }, + findBySlug: function(slug, dc) { + return get(this, 'store').queryRecord(this.getModelName(), { + id: slug, + dc: dc, + }); + }, + create: function(obj) { + // TODO: This should probably return a Promise + return get(this, 'store').createRecord(this.getModelName(), obj); + }, + persist: function(item) { + return item.save(); + }, + remove: function(obj) { + let item = obj; + if (typeof obj.destroyRecord === 'undefined') { + item = obj.get('data'); + } + if (typeOf(item) === 'object') { + item = get(this, 'store').peekRecord(this.getModelName(), item[this.getPrimaryKey()]); + } + return item.destroyRecord().then(item => { + return get(this, 'store').unloadRecord(item); + }); + }, + invalidate: function() { + get(this, 'store').unloadAll(this.getModelName()); + }, +}); diff --git a/ui-v2/app/styles/app.scss b/ui-v2/app/styles/app.scss index 61fd865901..ce7dc4ba16 100644 --- a/ui-v2/app/styles/app.scss +++ b/ui-v2/app/styles/app.scss @@ -6,32 +6,7 @@ @import 'ember-power-select'; -@import 'components/breadcrumbs'; -@import 'components/anchors'; -@import 'components/buttons'; -@import 'components/tabs'; -@import 'components/pill'; -@import 'components/table'; -@import 'components/form-elements'; - -@import 'components/tabular-collection'; -@import 'components/list-collection'; - -@import 'components/product'; - -@import 'components/healthcheck-status'; -@import 'components/healthchecked-resource'; -@import 'components/freetext-filter'; -@import 'components/filter-bar'; -@import 'components/tomography-graph'; -@import 'components/action-group'; -@import 'components/flash-message'; -@import 'components/code-editor'; -@import 'components/confirmation-dialog'; -@import 'components/feedback-dialog'; -@import 'components/notice'; -@import 'components/with-tooltip'; - +@import 'components/index'; @import 'core/typography'; @import 'core/layout'; @@ -40,3 +15,5 @@ @import 'routes/dc/intention/index'; @import 'routes/dc/kv/index'; @import 'routes/dc/acls/index'; +@import 'routes/dc/acls/tokens/index'; +@import 'routes/dc/acls/policies/index'; diff --git a/ui-v2/app/styles/base/icons/index.scss b/ui-v2/app/styles/base/icons/index.scss index 05385a0a50..9d228363d3 100644 --- a/ui-v2/app/styles/base/icons/index.scss +++ b/ui-v2/app/styles/base/icons/index.scss @@ -1,5 +1,14 @@ +$star-svg: url('data:image/svg+xml;charset=UTF-8,'); +$eye-svg: url('data:image/svg+xml;charset=UTF-8,'); + +$chevron-svg: url('data:image/svg+xml;charset=UTF-8,'); + +$cancel-plain-svg: url('data:image/svg+xml;charset=UTF-8,'); + +$loading-svg: url('data:image/svg+xml;charset=UTF-8,'); + $hashicorp-svg: url('data:image/svg+xml;charset=UTF-8,'); $consul-color-svg: url('data:image/svg+xml;charset=UTF-8,'); $nomad-color-svg: url('data:image/svg+xml;charset=UTF-8,'); -$terraform-color-svg: url('data:image/svg+xml;charset=UTF-8,'); \ No newline at end of file +$terraform-color-svg: url('data:image/svg+xml;charset=UTF-8,'); diff --git a/ui-v2/app/styles/base/reset/base-variables.scss b/ui-v2/app/styles/base/reset/base-variables.scss index e726cdd98e..2299fd0bfe 100644 --- a/ui-v2/app/styles/base/reset/base-variables.scss +++ b/ui-v2/app/styles/base/reset/base-variables.scss @@ -33,3 +33,10 @@ -ms-user-select: none; user-select: none; } +%user-select-text { + -webkit-touch-callout: default; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} diff --git a/ui-v2/app/styles/base/reset/system.scss b/ui-v2/app/styles/base/reset/system.scss index 72af44357e..27d5d3797b 100644 --- a/ui-v2/app/styles/base/reset/system.scss +++ b/ui-v2/app/styles/base/reset/system.scss @@ -74,7 +74,9 @@ fieldset { border: none; width: 100%; } -a { +a, +input[type='checkbox'], +input[type='radio'] { cursor: pointer; } hr { diff --git a/ui-v2/app/styles/base/typography/base-variables.scss b/ui-v2/app/styles/base/typography/base-variables.scss index 7b1faff76f..1d7b7ad13f 100644 --- a/ui-v2/app/styles/base/typography/base-variables.scss +++ b/ui-v2/app/styles/base/typography/base-variables.scss @@ -3,10 +3,10 @@ $typo-family-sans: BlinkMacSystemFont, -apple-system, 'Segoe UI', 'Roboto', 'Oxy $typo-family-mono: monospace; $typo-size-000: 16px; $typo-size-100: 3.5rem; -$typo-size-200: 2.5rem; -$typo-size-300: 2.2rem; -$typo-size-400: 1.5rem; -$typo-size-500: 1.125rem; +$typo-size-200: 1.8rem; +$typo-size-300: 1.3rem; +$typo-size-400: 1.2rem; +$typo-size-500: 1rem; $typo-size-600: 0.875rem; $typo-size-700: 0.8125rem; $typo-size-800: 0.75rem; diff --git a/ui-v2/app/styles/base/typography/index.scss b/ui-v2/app/styles/base/typography/index.scss index bb221c7e8f..6da895b1bf 100644 --- a/ui-v2/app/styles/base/typography/index.scss +++ b/ui-v2/app/styles/base/typography/index.scss @@ -1 +1,2 @@ @import './base-variables'; +@import './semantic-variables'; diff --git a/ui-v2/app/styles/base/typography/semantic-variables.scss b/ui-v2/app/styles/base/typography/semantic-variables.scss new file mode 100644 index 0000000000..48cc6cb3e9 --- /dev/null +++ b/ui-v2/app/styles/base/typography/semantic-variables.scss @@ -0,0 +1,3 @@ +$typo-header-100: $typo-size-200; +$typo-header-200: $typo-size-300; +$typo-header-300: $typo-size-500; diff --git a/ui-v2/app/styles/components/action-group/layout.scss b/ui-v2/app/styles/components/action-group/layout.scss index 0c12687a0d..06a47fb2ab 100644 --- a/ui-v2/app/styles/components/action-group/layout.scss +++ b/ui-v2/app/styles/components/action-group/layout.scss @@ -27,6 +27,7 @@ z-index: -1; top: 0; } +/* this is actually the group */ %action-group ul { position: absolute; right: -10px; @@ -71,6 +72,6 @@ %action-group input[type='radio']:checked ~ .with-confirmation > ul { display: block; } -%action-group input[type='radio']:checked ~ label[for="actions_close"] { +%action-group input[type='radio']:checked ~ label[for='actions_close'] { z-index: 1; } diff --git a/ui-v2/app/styles/components/action-group/skin.scss b/ui-v2/app/styles/components/action-group/skin.scss index 29881072b8..ac46082e59 100644 --- a/ui-v2/app/styles/components/action-group/skin.scss +++ b/ui-v2/app/styles/components/action-group/skin.scss @@ -1,7 +1,17 @@ +%action-group label:first-of-type { + @extend %toggle-button; +} +%action-group input[type='radio']:checked + label:first-of-type { + background-color: $ui-gray-050; +} %action-group label { - border-radius: $radius-small; cursor: pointer; } +%action-group label::after, +%action-group label::before, +%action-group::before { + @extend %with-dot; +} %action-group ul { border: $decor-border-100; border-radius: $radius-small; @@ -15,13 +25,6 @@ %action-group ul::before { border-color: $ui-color-action; } -%action-group input[type='radio']:checked + label:first-of-type, -%action-group label:first-of-type:hover { - background-color: $ui-gray-050; -} -%action-group label:first-of-type:active { - background-color: $ui-gray-100; -} %action-group li a:hover { background-color: $ui-color-action; color: $ui-white; @@ -30,8 +33,3 @@ %action-group ul::before { background-color: $ui-white; } -%action-group label::after, -%action-group label::before, -%action-group::before { - @extend %with-dot; -} diff --git a/ui-v2/app/styles/components/anchors.scss b/ui-v2/app/styles/components/anchors.scss index 4c0399dd69..3d26d0764b 100644 --- a/ui-v2/app/styles/components/anchors.scss +++ b/ui-v2/app/styles/components/anchors.scss @@ -1,18 +1,18 @@ @import './anchors/index'; -main a { +%main-content a { color: $ui-gray-900; } -main a[rel*='help'] { +%main-content a[rel*='help'] { @extend %with-info; } -main label a[rel*='help'] { +%main-content label a[rel*='help'] { color: $ui-gray-400; } [role='tabpanel'] > p:only-child [rel*='help']::after { content: none; } -main p a, -main dd a { +%main-content p a, +%main-content dd a { @extend %anchor; } diff --git a/ui-v2/app/styles/components/app-view.scss b/ui-v2/app/styles/components/app-view.scss new file mode 100644 index 0000000000..8b4c559755 --- /dev/null +++ b/ui-v2/app/styles/components/app-view.scss @@ -0,0 +1,40 @@ +@import './app-view/index'; +@import './filter-bar/index'; +@import './buttons/index'; +main { + @extend %app-view; +} +%app-view > div > div { + @extend %app-content; +} +%app-view header form { + @extend %filter-bar; +} +@media #{$--lt-spacious-page-header} { + %app-view header .actions { + margin-top: 5px; + } +} +%app-view h1 span { + @extend %with-external-source-icon; +} +%app-view header .actions a, +%app-view header .actions button { + @extend %button-compact; +} +%app-content div > dl { + @extend %form-row; +} +[role='tabpanel'] > p:only-child, +.template-error > div, +%app-content > p:only-child, +%app-view > div.disabled > div, +%app-view.empty > div { + @extend %app-content-empty; +} +[role='tabpanel'] > *:first-child { + margin-top: 1.25em; +} +%app-view > div.disabled > div { + margin-top: 0 !important; +} diff --git a/ui-v2/app/styles/components/app-view/index.scss b/ui-v2/app/styles/components/app-view/index.scss new file mode 100644 index 0000000000..bc18252196 --- /dev/null +++ b/ui-v2/app/styles/components/app-view/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/styles/components/app-view/layout.scss b/ui-v2/app/styles/components/app-view/layout.scss new file mode 100644 index 0000000000..34827cc5bf --- /dev/null +++ b/ui-v2/app/styles/components/app-view/layout.scss @@ -0,0 +1,52 @@ +/* layout */ +%app-view header > div:last-of-type > div:first-child { + flex-grow: 1; +} +%app-view { + position: relative; +} +%app-view header .actions { + float: right; + display: flex; + align-items: flex-start; +} +/* units */ +%app-view { + margin-top: 50px; +} +%app-view header + div > *:first-child { + margin-top: 1.8em; +} +%app-view h2 { + padding-bottom: 0.2em; + margin-bottom: 1.1em; +} +%app-view header .actions > *:not(:last-child) { + margin-right: 12px; +} + +// content +%app-content div > dl > dt { + position: absolute; +} +%app-content div > dl { + position: relative; +} +%app-content-empty { + margin-top: 0; + padding: 50px; + text-align: center; +} +%app-content form:not(:last-child) { + margin-bottom: 2.2em; +} +%app-content div > dl > dt { + width: 140px; +} +%app-content div > dl > dd { + padding-left: 140px; +} +%app-content div > dl > * { + min-height: 1em; + margin-bottom: 0.4em; +} diff --git a/ui-v2/app/styles/components/app-view/skin.scss b/ui-v2/app/styles/components/app-view/skin.scss new file mode 100644 index 0000000000..ab4fd115d4 --- /dev/null +++ b/ui-v2/app/styles/components/app-view/skin.scss @@ -0,0 +1,21 @@ +%app-view h2, +%app-view header > div:last-of-type { + border-bottom: $decor-border-100; +} +%app-view header > div:last-of-type, +%app-view h2 { + border-color: $keyline-light; +} +%app-content div > dl > dd { + color: $ui-gray-400; +} +[role='tabpanel'] > p:only-child, +.template-error > div { + background-color: $ui-gray-050; + color: $ui-gray-400; +} +%app-content > p:only-child, +%app-view > div.disabled > div { + background-color: $ui-gray-050; + color: $ui-gray-400; +} diff --git a/ui-v2/app/styles/components/breadcrumbs/layout.scss b/ui-v2/app/styles/components/breadcrumbs/layout.scss index fff6770491..8b13474d80 100644 --- a/ui-v2/app/styles/components/breadcrumbs/layout.scss +++ b/ui-v2/app/styles/components/breadcrumbs/layout.scss @@ -1,6 +1,6 @@ %breadcrumbs { position: absolute; - top: -35px; // %app-view:margin-top - 15px; + top: -38px; // %app-view:margin-top - 15px; } %breadcrumbs ol { display: flex; diff --git a/ui-v2/app/styles/components/buttons/layout.scss b/ui-v2/app/styles/components/buttons/layout.scss index ab1ff50cc9..249ef1c502 100644 --- a/ui-v2/app/styles/components/buttons/layout.scss +++ b/ui-v2/app/styles/components/buttons/layout.scss @@ -1,17 +1,40 @@ %button { + position: relative; +} +%button .progress.indeterminate { + position: absolute; + top: 50%; + left: 50%; + margin-left: -12px; + margin-top: -12px; +} +%button:disabled .progress + * { + visibility: hidden; +} +%button:empty { + padding-right: 0 !important; + padding-left: 18px !important; + margin-right: 5px; +} +%button:empty::before { + left: 1px; +} +%button:not(:empty) { display: inline-flex; text-align: center; justify-content: center; align-items: center; padding: calc(0.375em - 1px) calc(2.2em - 1px); - height: 2.5em; + height: 2.55em; + min-width: 100px; } %button:not(:last-child) { - margin-right: 7px; + margin-right: 8px; } %button-compact { // @extend %button; - padding-left: calc(1.75em - 1px); - padding-right: calc(1.75em - 1px); - height: 2.1em; + padding-left: calc(1.6em - 1px) !important; + padding-right: calc(1.6em - 1px) !important; + padding-top: calc(0.35em - 1px) !important; + height: 2.3em !important; } diff --git a/ui-v2/app/styles/components/buttons/skin.scss b/ui-v2/app/styles/components/buttons/skin.scss index b79812540c..af1af1b0b1 100644 --- a/ui-v2/app/styles/components/buttons/skin.scss +++ b/ui-v2/app/styles/components/buttons/skin.scss @@ -10,6 +10,10 @@ } %copy-button { @extend %button, %with-clipboard; + min-height: 17px; +} +%copy-button:not(:empty) { + padding-left: 38px !important; } %primary-button, %secondary-button, diff --git a/ui-v2/app/styles/components/checkbox-group/index.scss b/ui-v2/app/styles/components/checkbox-group/index.scss new file mode 100644 index 0000000000..bc18252196 --- /dev/null +++ b/ui-v2/app/styles/components/checkbox-group/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/styles/components/checkbox-group/layout.scss b/ui-v2/app/styles/components/checkbox-group/layout.scss new file mode 100644 index 0000000000..903108fe85 --- /dev/null +++ b/ui-v2/app/styles/components/checkbox-group/layout.scss @@ -0,0 +1,9 @@ +%checkbox-group span { + display: inline-block; + margin-left: 10px; + min-width: 50px; +} +%checkbox-group label { + margin-right: 10px; + white-space: nowrap; +} diff --git a/ui-v2/app/styles/components/checkbox-group/skin.scss b/ui-v2/app/styles/components/checkbox-group/skin.scss new file mode 100644 index 0000000000..5ae8b1a222 --- /dev/null +++ b/ui-v2/app/styles/components/checkbox-group/skin.scss @@ -0,0 +1,3 @@ +%checkbox-group label { + cursor: pointer; +} diff --git a/ui-v2/app/styles/components/confirmation-dialog.scss b/ui-v2/app/styles/components/confirmation-dialog.scss index 02ee72815e..1a039f8bb9 100644 --- a/ui-v2/app/styles/components/confirmation-dialog.scss +++ b/ui-v2/app/styles/components/confirmation-dialog.scss @@ -2,7 +2,7 @@ div.with-confirmation { @extend %confirmation-dialog, %confirmation-dialog-inline; } -table div.with-confirmation.confirming { +table td > div.with-confirmation.confirming { position: absolute; right: 0; } diff --git a/ui-v2/app/styles/components/confirmation-dialog/layout.scss b/ui-v2/app/styles/components/confirmation-dialog/layout.scss index 798ebecca9..29c0cd12ac 100644 --- a/ui-v2/app/styles/components/confirmation-dialog/layout.scss +++ b/ui-v2/app/styles/components/confirmation-dialog/layout.scss @@ -2,7 +2,10 @@ float: right; } %confirmation-dialog-inline p { - margin-right: 1em; + margin-right: 12px; + padding-left: 12px; + padding-top: 5px; + padding-bottom: 5px; margin-bottom: 0; } %confirmation-dialog-inline { diff --git a/ui-v2/app/styles/components/dom-recycling-table/index.scss b/ui-v2/app/styles/components/dom-recycling-table/index.scss new file mode 100644 index 0000000000..0820684ccb --- /dev/null +++ b/ui-v2/app/styles/components/dom-recycling-table/index.scss @@ -0,0 +1 @@ +@import './layout'; diff --git a/ui-v2/app/styles/components/dom-recycling-table/layout.scss b/ui-v2/app/styles/components/dom-recycling-table/layout.scss new file mode 100644 index 0000000000..ecd1c52ab9 --- /dev/null +++ b/ui-v2/app/styles/components/dom-recycling-table/layout.scss @@ -0,0 +1,13 @@ +%dom-recycling-table { + position: relative; +} +%dom-recycling-table tr { + display: flex; +} +%dom-recycling-table tr > * { + flex: 1 0 auto; +} +%dom-recycling-table tbody { + /* important required as ember-collection will inline an overflow: visible*/ + overflow-x: hidden !important; +} diff --git a/ui-v2/app/styles/components/flash-message.scss b/ui-v2/app/styles/components/flash-message.scss index 1043b10919..13a2334f74 100644 --- a/ui-v2/app/styles/components/flash-message.scss +++ b/ui-v2/app/styles/components/flash-message.scss @@ -2,3 +2,6 @@ .flash-message { @extend %flash-message; } +%flash-message.exiting { + @extend %blink-in-fade-out; +} diff --git a/ui-v2/app/styles/components/form-elements.scss b/ui-v2/app/styles/components/form-elements.scss index a9095a387c..942efbb8cb 100644 --- a/ui-v2/app/styles/components/form-elements.scss +++ b/ui-v2/app/styles/components/form-elements.scss @@ -1,18 +1,38 @@ +/*TODO: This remains a mix of form-elements */ +/* form-elements should probably be a collection of these */ @import './form-elements/index'; @import './toggle/index'; -.type-toggle { - @extend %toggle; -} +@import './radio-group/index'; +@import './checkbox-group/index'; label span { @extend %user-select-none; } .has-error { @extend %form-element-error; } -%app-content .type-text, -%app-content .type-toggle { +%modal-dialog .type-text, +%app-content .type-text { @extend %form-element; } +.type-toggle { + @extend %form-element, %toggle; +} +%form-element, +%radio-group, +%checkbox-group, +form table, +%app-content form dl { + @extend %form-row; +} %app-content [role='radiogroup'] { @extend %radio-group; } +%radio-group label { + @extend %form-element; +} +.checkbox-group { + @extend %checkbox-group; +} +%toggle + .checkbox-group { + margin-top: -1em; +} diff --git a/ui-v2/app/styles/components/form-elements/layout.scss b/ui-v2/app/styles/components/form-elements/layout.scss index b1ae6d3501..0f52ccfa76 100644 --- a/ui-v2/app/styles/components/form-elements/layout.scss +++ b/ui-v2/app/styles/components/form-elements/layout.scss @@ -1,9 +1,18 @@ +%form-row { + margin-bottom: 1.4em; +} +%form-element { + @extend %form-row; +} %form-element, %form-element > em, %form-element > span, %form-element textarea { display: block; } +%form-element a { + display: inline; +} %form-element > em > code { display: inline-block; } @@ -18,6 +27,10 @@ %form-element > span { margin-bottom: 0.5em; } +%form-element > span + em { + margin-top: -0.5em; + margin-bottom: 0.5em; +} %form-element textarea { max-width: 100%; min-width: 100%; @@ -47,30 +60,3 @@ %form-element > span { margin-bottom: 0.4em !important; } -%form-element, -%radio-group { - margin-bottom: 1.55em; -} -%radio-group { - overflow: hidden; -} -%radio-group label { - float: left; -} -%radio-group label > span { - float: right; -} -%radio-group { - padding-left: 1px; -} -%radio-group label:not(:last-child) { - margin-right: 25px; -} -%radio-group label > span { - margin-left: 1em; - margin-top: 0.2em; -} -%radio-group label, -%radio-group label > span { - margin-bottom: 0 !important; -} diff --git a/ui-v2/app/styles/components/form-elements/skin.scss b/ui-v2/app/styles/components/form-elements/skin.scss index 9c6cbe49ee..1b76786336 100644 --- a/ui-v2/app/styles/components/form-elements/skin.scss +++ b/ui-v2/app/styles/components/form-elements/skin.scss @@ -1,5 +1,5 @@ -%radio-group label { - @extend %form-element; +%form-element > strong { + @extend %with-error; } %form-element-error > input, %form-element-error > textarea { @@ -10,7 +10,7 @@ %form-element textarea { -moz-appearance: none; -webkit-appearance: none; - box-shadow: inset 0 4px 1px rgba(0, 0, 0, .06); + box-shadow: inset 0 4px 1px rgba(0, 0, 0, 0.06); border-radius: $decor-radius-100; border: $decor-border-100; } @@ -25,6 +25,9 @@ %form-element-error > input { border-color: $ui-color-failure !important; } +%form-element > strong { + color: $ui-color-failure; +} %form-element > em { color: $ui-gray-400; } diff --git a/ui-v2/app/styles/components/icons/index.scss b/ui-v2/app/styles/components/icons/index.scss index c54a5f59f7..f0f110694a 100644 --- a/ui-v2/app/styles/components/icons/index.scss +++ b/ui-v2/app/styles/components/icons/index.scss @@ -1,27 +1,34 @@ /*TODO: The old pseudo-icon was to specific */ /* make a temporary one with the -- prefix */ /* to make it more reusable temporarily */ +%bg-icon { + background-repeat: no-repeat; + background-position: center; +} %--pseudo-icon { - display: block; + display: inline-block; content: ''; visibility: visible; - position: absolute; - top: 50%; background-repeat: no-repeat; - background-position: center center; + background-position: center; } %pseudo-icon-bg-img { @extend %--pseudo-icon; + position: relative; background-size: contain; background-color: transparent; } %pseudo-icon-css { @extend %--pseudo-icon; + display: block; + position: absolute; + top: 50%; width: 1em; height: 1em; margin-top: -0.6em; background-color: currentColor; } +/* %pseudo-icon-mask, %pseudo-icon-overlay ?*/ %pseudo-icon { @extend %pseudo-icon-css; } @@ -145,6 +152,36 @@ width: 16px; height: 16px; } +/*TODO: All chevrons need merging */ +%with-chevron-down::before { + @extend %pseudo-icon-bg-img; + background-image: $chevron-svg; + width: 10px; + height: 6px; +} +%with-star-before::before, +%with-star-after::after { + @extend %pseudo-icon-bg-img; + background-image: $star-svg; + width: 10px; + height: 9px; +} +%with-star-before::before { + padding-right: 12px; +} +%with-star-after::after { + padding-left: 22px; +} +%with-star { + @extend %with-star-before; +} +%with-eye::before { + @extend %pseudo-icon-bg-img; + background-image: $eye-svg; + width: 16px; + height: 8px; + padding-right: 12px; +} %with-tick { @extend %pseudo-icon; background-image: url('data:image/svg+xml;charset=UTF-8,'); @@ -216,3 +253,11 @@ @extend %with-minus; border-radius: 20%; } +%with-error { + position: relative; + padding-left: 18px; +} +%with-error::before { + @extend %with-cross; + margin-top: -0.5em; +} diff --git a/ui-v2/app/styles/components/index.scss b/ui-v2/app/styles/components/index.scss new file mode 100644 index 0000000000..f461f7bea1 --- /dev/null +++ b/ui-v2/app/styles/components/index.scss @@ -0,0 +1,31 @@ +@import './breadcrumbs'; +@import './anchors'; +@import './progress'; +@import './buttons'; +@import './toggle-button'; +@import './secret-button'; +@import './tabs'; +@import './pill'; +@import './table'; +@import './form-elements'; + +@import './tabular-details'; +@import './tabular-collection'; +@import './list-collection'; + +@import './app-view'; +@import './product'; + +@import './healthcheck-status'; +@import './healthchecked-resource'; +@import './freetext-filter'; +@import './filter-bar'; +@import './tomography-graph'; +@import './action-group'; +@import './flash-message'; +@import './code-editor'; +@import './confirmation-dialog'; +@import './feedback-dialog'; +@import './modal-dialog'; +@import './notice'; +@import './with-tooltip'; diff --git a/ui-v2/app/styles/components/modal-dialog.scss b/ui-v2/app/styles/components/modal-dialog.scss new file mode 100644 index 0000000000..a194462ce3 --- /dev/null +++ b/ui-v2/app/styles/components/modal-dialog.scss @@ -0,0 +1,13 @@ +@import './modal-dialog/index'; +[role='dialog'] { + @extend %modal-dialog; +} +input[name='modal'] { + @extend %modal-control; +} +html.template-with-modal { + @extend %with-modal; +} +%modal-dialog table { + min-height: 149px; +} diff --git a/ui-v2/app/styles/components/modal-dialog/index.scss b/ui-v2/app/styles/components/modal-dialog/index.scss new file mode 100644 index 0000000000..bc18252196 --- /dev/null +++ b/ui-v2/app/styles/components/modal-dialog/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/styles/components/modal-dialog/layout.scss b/ui-v2/app/styles/components/modal-dialog/layout.scss new file mode 100644 index 0000000000..ee3ed9d167 --- /dev/null +++ b/ui-v2/app/styles/components/modal-dialog/layout.scss @@ -0,0 +1,70 @@ +%modal-dialog > div > div { + @extend %modal-window; +} +%with-modal { + overflow: hidden; +} +%modal-dialog { + z-index: 10000; + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; +} +%modal-control, +%modal-control + * { + display: none; +} +%modal-control:checked + * { + display: block; +} +%modal-dialog > label { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +%modal-dialog > div { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +%modal-window.overflowing { + overflow: auto; + height: 100%; +} +%modal-window { + max-width: 855px; + position: relative; + z-index: 1; +} +%modal-window > * { + padding-left: 15px; + padding-right: 15px; +} +%modal-window > div { + padding: 20px 23px; +} +%modal-window > footer, +%modal-window > header { + padding-top: 12px; + padding-bottom: 10px; +} +%modal-window table { + height: 150px !important; +} +%modal-window tbody { + max-height: 100px; +} +%modal-window > header { + position: relative; +} +%modal-window > header [for='modal_close'] { + float: right; + text-indent: -9000px; + width: 23px; + height: 23px; +} diff --git a/ui-v2/app/styles/components/modal-dialog/skin.scss b/ui-v2/app/styles/components/modal-dialog/skin.scss new file mode 100644 index 0000000000..bd901019de --- /dev/null +++ b/ui-v2/app/styles/components/modal-dialog/skin.scss @@ -0,0 +1,42 @@ +%modal-dialog > label { + background-color: rgba($ui-white, 0.9); +} +%modal-window { + box-shadow: 2px 8px 8px 0 rgba($ui-black, 0.1); +} +%modal-window { + /*%frame-gray-000*/ + background-color: $ui-white; + border: $decor-border-100; + border-color: $ui-gray-300; +} +%modal-window > footer, +%modal-window > header { + /*%frame-gray-000*/ + border: 0 solid; + background-color: $ui-gray-050; + border-color: $ui-gray-300; +} +%modal-window > footer { + border-top-width: 1px; +} +%modal-window > header { + border-bottom-width: 1px; +} +%modal-window.warning > header { + @extend %with-warning; + text-indent: 20px; +} + +%modal-window > header [for='modal_close'] { + @extend %bg-icon; + background-image: $cancel-plain-svg; + background-size: 80%; + + cursor: pointer; + /*%frame-gray-000*/ + background-color: $ui-gray-050; + border: $decor-border-100; + border-color: $ui-gray-300; + border-radius: $decor-radius-100; +} diff --git a/ui-v2/app/styles/components/notice.scss b/ui-v2/app/styles/components/notice.scss index e7b986dbce..3d0a22d22e 100644 --- a/ui-v2/app/styles/components/notice.scss +++ b/ui-v2/app/styles/components/notice.scss @@ -2,3 +2,9 @@ .notice.warning { @extend %notice-warning; } +.notice.info { + @extend %notice-info; +} +.notice.policy-management { + @extend %notice-highlight; +} diff --git a/ui-v2/app/styles/components/notice/layout.scss b/ui-v2/app/styles/components/notice/layout.scss index 64b017dd31..2e101a21f1 100644 --- a/ui-v2/app/styles/components/notice/layout.scss +++ b/ui-v2/app/styles/components/notice/layout.scss @@ -1,10 +1,11 @@ -%notice::before { - left: 20px; - top: 18px; - margin-top: 0; -} %notice { position: relative; padding: 1em; padding-left: 45px; } +%notice::before { + position: absolute; + left: 20px; + top: 18px; + margin-top: 0; +} diff --git a/ui-v2/app/styles/components/notice/skin.scss b/ui-v2/app/styles/components/notice/skin.scss index 7944c9a408..fcba275786 100644 --- a/ui-v2/app/styles/components/notice/skin.scss +++ b/ui-v2/app/styles/components/notice/skin.scss @@ -4,6 +4,7 @@ } %notice-success, %notice-info, +%notice-highlight, %notice-error, %notice-warning { @extend %notice; @@ -18,14 +19,28 @@ color: $ui-color-success; } %notice-info { + /* %frame-blue-000*/ + border-style: solid; /*TODO: this can go once we are using a frame*/ @extend %with-passing; /* needs a better info button*/ background-color: $ui-blue-050; border-color: $ui-color-action; /* TODO: change to info */ color: $ui-blue-700; } +%notice-highlight { + /* %frame-blue-000*/ + border-style: solid; /*TODO: this can go once we are using a frame*/ + @extend %with-star; + border-color: $ui-gray-300; + background-color: $ui-gray-050; +} %notice-info::before { color: $ui-color-action; /* change to info */ } +%notice-highlight::before { + /* %with-star, bigger */ + width: 16px; + height: 16px; +} %notice-warning { @extend %frame-yellow-500, %with-warning; } diff --git a/ui-v2/app/styles/components/product.scss b/ui-v2/app/styles/components/product.scss index bde2b14d6c..4207add1a2 100644 --- a/ui-v2/app/styles/components/product.scss +++ b/ui-v2/app/styles/components/product.scss @@ -9,20 +9,6 @@ html.ember-loading body > svg { html.template-loading main > div { @extend %loader; } -main { - @extend %app-view; -} -%app-view > div > div { - @extend %app-content; -} -%app-view header form { - @extend %filter-bar; -} -@media #{$--lt-spacious-page-header} { - %app-view header .actions { - margin-top: 5px; - } -} %loader circle { fill: $brand-magenta-100; } diff --git a/ui-v2/app/styles/components/product/app-view.scss b/ui-v2/app/styles/components/product/app-view.scss deleted file mode 100644 index b466ea3707..0000000000 --- a/ui-v2/app/styles/components/product/app-view.scss +++ /dev/null @@ -1,87 +0,0 @@ -%app-view h2, -%app-view header > div:last-of-type { - border-bottom: $decor-border-100; -} -%app-view header > div:last-of-type, -%app-view h2 { - border-color: $keyline-light; -} -%app-content div > dl > dd { - color: $ui-gray-400; -} -[role='tabpanel'] > p:only-child, -.template-error > div { - background-color: $ui-gray-050; - color: $ui-gray-400; -} -%app-content > p:only-child, -%app-view > div.disabled > div { - background-color: $ui-gray-050; - color: $ui-gray-400; -} -%app-view header > div:last-of-type > div:first-child { - flex-grow: 1; -} -%app-view { - position: relative; -} -%app-view header .actions { - float: right; - display: flex; - align-items: flex-start; -} -%app-view h1 span { - @extend %with-external-source-icon; -} -%app-view { - margin-top: 50px; -} -%app-view h2 { - padding-bottom: 0.2em; - margin-bottom: 1.1em; -} -%app-view header + div > *:first-child { - margin-top: 1.25em; -} -%app-view header .actions > *:not(:last-child) { - margin-right: 7px; -} -%app-view header .actions a, -%app-view header .actions button { - @extend %button-compact; -} - -// content -%app-content div > dl > dt { - position: absolute; -} -%app-content div > dl { - position: relative; -} -[role='tabpanel'] > p:only-child, -.template-error > div, -%app-content > p:only-child, -%app-view > div.disabled > div { - margin-top: 0; - padding: 50px; - text-align: center; -} -[role='tabpanel'] > *:first-child { - margin-top: 1.25em; -} -%app-view > div.disabled > div { - margin-top: 0 !important; -} -%app-content form:not(:last-child) { - margin-bottom: 2.2em; -} -%app-content div > dl > dt { - width: 120px; -} -%app-content div > dl > dd { - padding-left: 120px; -} -%app-content div > dl > * { - min-height: 1em; - margin-bottom: 0.3em; -} diff --git a/ui-v2/app/styles/components/product/footer.scss b/ui-v2/app/styles/components/product/footer.scss index aafe86f8aa..25d61c6342 100644 --- a/ui-v2/app/styles/components/product/footer.scss +++ b/ui-v2/app/styles/components/product/footer.scss @@ -22,14 +22,14 @@ } @media #{$--tall-footer} { %footer { - padding-top: 25px; - padding-bottom: 25px; + padding-top: 12px; + padding-bottom: 12px; } } @media #{$--wide-footer} { %footer { - padding-left: 25px; - padding-right: 25px; + padding-left: 12px; + padding-right: 12px; } %footer > * { padding: 11px; diff --git a/ui-v2/app/styles/components/product/index.scss b/ui-v2/app/styles/components/product/index.scss index 02e434a49e..28e69d4795 100644 --- a/ui-v2/app/styles/components/product/index.scss +++ b/ui-v2/app/styles/components/product/index.scss @@ -5,5 +5,4 @@ @import './loader'; @import './main-header'; @import './header-nav'; -@import './app-view'; @import './footer'; diff --git a/ui-v2/app/styles/components/progress.scss b/ui-v2/app/styles/components/progress.scss new file mode 100644 index 0000000000..c625778ca5 --- /dev/null +++ b/ui-v2/app/styles/components/progress.scss @@ -0,0 +1,4 @@ +@import './progress/index'; +.progress.indeterminate { + @extend %progress-indeterminate; +} diff --git a/ui-v2/app/styles/components/progress/index.scss b/ui-v2/app/styles/components/progress/index.scss new file mode 100644 index 0000000000..bc18252196 --- /dev/null +++ b/ui-v2/app/styles/components/progress/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/styles/components/progress/layout.scss b/ui-v2/app/styles/components/progress/layout.scss new file mode 100644 index 0000000000..53c7f73982 --- /dev/null +++ b/ui-v2/app/styles/components/progress/layout.scss @@ -0,0 +1,13 @@ +%progress-native { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} +%progress-native::-webkit-progress-bar, +%progress-native::-webkit-progress-value { + display: none; +} +%progress-indeterminate { + height: 24px; + width: 24px; +} diff --git a/ui-v2/app/styles/components/progress/skin.scss b/ui-v2/app/styles/components/progress/skin.scss new file mode 100644 index 0000000000..2d8dd6c525 --- /dev/null +++ b/ui-v2/app/styles/components/progress/skin.scss @@ -0,0 +1,6 @@ +%progress-indeterminate { + background-image: $loading-svg; + background-position: center; + background-repeat: no-repeat; + background-color: transparent; +} diff --git a/ui-v2/app/styles/components/radio-group/index.scss b/ui-v2/app/styles/components/radio-group/index.scss new file mode 100644 index 0000000000..0820684ccb --- /dev/null +++ b/ui-v2/app/styles/components/radio-group/index.scss @@ -0,0 +1 @@ +@import './layout'; diff --git a/ui-v2/app/styles/components/radio-group/layout.scss b/ui-v2/app/styles/components/radio-group/layout.scss new file mode 100644 index 0000000000..120a5bf4ce --- /dev/null +++ b/ui-v2/app/styles/components/radio-group/layout.scss @@ -0,0 +1,23 @@ +%radio-group { + overflow: hidden; +} +%radio-group label { + float: left; +} +%radio-group label > span { + float: right; +} +%radio-group { + padding-left: 1px; +} +%radio-group label:not(:last-child) { + margin-right: 25px; +} +%radio-group label > span { + margin-left: 1em; + margin-top: 0.2em; +} +%radio-group label, +%radio-group label > span { + margin-bottom: 0 !important; +} diff --git a/ui-v2/app/styles/components/secret-button.scss b/ui-v2/app/styles/components/secret-button.scss new file mode 100644 index 0000000000..1a84124a77 --- /dev/null +++ b/ui-v2/app/styles/components/secret-button.scss @@ -0,0 +1,11 @@ +@import './secret-button/index'; +.type-reveal { + @extend %secret-button; +} +%secret-button { + visibility: hidden; + @extend %with-eye; +} +%secret-button span { + position: absolute; +} diff --git a/ui-v2/app/styles/components/secret-button/index.scss b/ui-v2/app/styles/components/secret-button/index.scss new file mode 100644 index 0000000000..bc18252196 --- /dev/null +++ b/ui-v2/app/styles/components/secret-button/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/styles/components/secret-button/layout.scss b/ui-v2/app/styles/components/secret-button/layout.scss new file mode 100644 index 0000000000..20b9eb5127 --- /dev/null +++ b/ui-v2/app/styles/components/secret-button/layout.scss @@ -0,0 +1,23 @@ +%secret-button { + cursor: pointer; +} +%secret-button input { + display: none; +} +%secret-button input + em { + visibility: hidden; + font-style: normal; +} +%secret-button input:checked + em { + @extend %user-select-text; + visibility: visible; + cursor: auto; +} +%secret-button input + em::before { + display: inline; + visibility: visible; + content: 'â–  â–  â–  â–  â–  â–  â–  â–  â–  â–  â–  â– '; +} +%secret-button input:checked + em::before { + display: none; +} diff --git a/ui-v2/app/styles/components/secret-button/skin.scss b/ui-v2/app/styles/components/secret-button/skin.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui-v2/app/styles/components/table.scss b/ui-v2/app/styles/components/table.scss index 482e2833aa..04519486eb 100644 --- a/ui-v2/app/styles/components/table.scss +++ b/ui-v2/app/styles/components/table.scss @@ -1,4 +1,5 @@ @import './icons/index'; +@import './table/index'; td.folder { @extend %with-folder; } @@ -20,9 +21,8 @@ td span.zero { table:not(.sessions) tr { cursor: pointer; } -th, -td { - border-bottom: $decor-border-100; +table:not(.sessions) td:first-child { + padding: 0; } td dt.passing, td dt.passing + dd { @@ -36,121 +36,3 @@ td dt.critical, td dt.critical + dd { color: $ui-color-failure; } -th { - color: $ui-gray-400 !important; -} -th { - border-color: $keyline-dark; -} -td { - border-color: $keyline-mid; -} -table { - width: 100%; -} -td button { - position: relative; - top: -6px; -} -th.actions input { - display: none; -} -th.actions { - text-align: right; -} -td.actions .with-confirmation.confirming { - position: absolute; - right: 0; -} -table th { - padding-bottom: 0.6em; -} -table td, -table td a { - padding: 0.9em 0; -} -table th, -table td:not(.actions), -table td a { - padding-right: 0.9em; -} -table:not(.sessions) td:first-child { - padding: 0; -} -table td a { - display: block; -} -tbody { - /* important required as ember-collection will inline an overflow: visible*/ - overflow-x: hidden !important; -} -th, -td:not(.actions), -td:not(.actions) a { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; -} -// TODO: this isn't specific to table -td dl { - height: 100%; -} -td dl { - display: flex; -} -td dl > * { - display: block; -} -td dt.zero { - display: none; -} -td dd.zero { - visibility: hidden; -} -td dt { - text-indent: -9000px; -} -td dt.warning { - overflow: visible; -} -td dt.warning::before { - top: 7px; -} -td dt.warning::after { - left: -2px; - top: -1px; -} -td dd { - box-sizing: content-box; - margin-left: 22px; - padding-right: 10px; -} -/* hide actions on narrow screens, you can always click in do everything from there */ -@media #{$--lt-wide-table} { - tr > .actions { - display: none; - } -} -/* ideally these would be in route css files, but left here as they */ -/* accomplish the same thing (hide non-essential columns for tables) */ -@media #{$--lt-wide-table} { - html.template-intention.template-list tr > :nth-last-child(2) { - display: none; - } - html.template-service.template-list tr > :last-child { - display: none; - } - html.template-node.template-show #services tr > :last-child { - display: none; - } - html.template-node.template-show #lock-sessions tr > - :not(:first-child):not(:last-child) { - display: none; - } - html.template-node.template-show #lock-sessions td:last-child { - padding: 0; - } - html.template-node.template-show #lock-sessions td:last-child button { - float: right; - } -} diff --git a/ui-v2/app/styles/components/table/index.scss b/ui-v2/app/styles/components/table/index.scss new file mode 100644 index 0000000000..bc18252196 --- /dev/null +++ b/ui-v2/app/styles/components/table/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/styles/components/table/layout.scss b/ui-v2/app/styles/components/table/layout.scss new file mode 100644 index 0000000000..23301b423e --- /dev/null +++ b/ui-v2/app/styles/components/table/layout.scss @@ -0,0 +1,124 @@ +table { + width: 100%; +} +%table-actions { + width: 60px; +} +th.actions input { + display: none; +} +th.actions { + text-align: right; +} +td.actions .with-confirmation.confirming { + position: absolute; + bottom: 4px; + right: 1px; +} +td.actions .with-confirmation.confirming p { + margin-bottom: 1em; +} +table caption { + text-align: left; + margin-bottom: 0.8em; +} +td > button, +td > .with-confirmation > button { + position: relative; + top: -6px; +} +table th { + padding-bottom: 0.6em; +} +table td, +table td a { + padding: 0.9em 0; +} +table th, +table td:not(.actions), +table td a { + padding-right: 0.9em; +} +table td a { + display: block; +} +th, +td:not(.actions), +td:not(.actions) a { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +// TODO: this isn't specific to table +// these are the node health 3 column display +tr > * dl { + float: left; +} +td dl { + height: 100%; +} +td dl { + display: flex; +} +td dl > * { + display: block; +} +td dt.zero { + display: none; +} +td dd.zero { + visibility: hidden; +} +td dt { + text-indent: -9000px; +} +td dt.warning { + overflow: visible; +} +td dt.warning::before { + top: 7px; +} +td dt.warning::after { + left: -2px; + top: -1px; +} +td dd { + box-sizing: content-box; + margin-left: 22px; + padding-right: 10px; +} +/* hide actions on narrow screens, you can always click in do everything from there */ +@media #{$--lt-wide-table} { + tr > .actions { + display: none; + } +} +/* ideally these would be in route css files, but left here as they */ +/* accomplish the same thing (hide non-essential columns for tables) */ +@media #{$--lt-medium-table} { + /* Policy > Datacenters */ + html.template-policy.template-list tr > :nth-child(2) { + display: none; + } +} +@media #{$--lt-wide-table} { + html.template-intention.template-list tr > :nth-last-child(2) { + display: none; + } + html.template-service.template-list tr > :last-child { + display: none; + } + html.template-node.template-show #services tr > :last-child { + display: none; + } + html.template-node.template-show #lock-sessions tr > :not(:first-child):not(:last-child) { + display: none; + } + html.template-node.template-show #lock-sessions td:last-child { + padding: 0; + } + html.template-node.template-show #lock-sessions td:last-child button { + float: right; + } +} diff --git a/ui-v2/app/styles/components/table/skin.scss b/ui-v2/app/styles/components/table/skin.scss new file mode 100644 index 0000000000..d6582bb793 --- /dev/null +++ b/ui-v2/app/styles/components/table/skin.scss @@ -0,0 +1,13 @@ +th, +td { + border-bottom: $decor-border-100; +} +th { + color: $ui-gray-400 !important; +} +th { + border-color: $keyline-dark; +} +td { + border-color: $keyline-mid; +} diff --git a/ui-v2/app/styles/components/tabular-collection.scss b/ui-v2/app/styles/components/tabular-collection.scss index 3999fe20a0..c9745e787f 100644 --- a/ui-v2/app/styles/components/tabular-collection.scss +++ b/ui-v2/app/styles/components/tabular-collection.scss @@ -1,25 +1,40 @@ -table { - position: relative; +@import './dom-recycling-table/index'; +table.dom-recycling { + @extend %dom-recycling-table; } -table tbody { - width: 100%; - height: 100%; +/* project specific */ +%dom-recycling-table { + /* minimum of 4x50px heigh rows plus top/bottom margins*/ + min-height: 249px; +} +%dom-recycling-table tbody { + /* tbodys are all absolute so,*/ + /* make room for the header */ top: 29px !important; + /* Make room for the header, plus 20px for a margin on the bottom */ + width: 100%; } -table tr { - display: flex; -} -table tr > * { - flex: 1 0 auto; +%dom-recycling-table caption ~ tbody { + /* tbodys are all absolute so,*/ + /* make room for the header */ + top: 57px !important; + /* Make room for the header, plus 20px for a margin on the bottom */ } -tr > * dl { - float: left; -} /* TODO: putting this here is less than ideal */ /* but this is another area where I am specifically */ /* targetting table-like things. This is now a prime */ /* area for a bit of refactoring/reorganizing */ + +/* Every type of 'row' is given a placeholder which */ +/* can apply to all th's and td's in the table */ +/* (the placeholders refer to a tf so `> *` will get you */ +/* both th and td). +/* Next, all the below calculations let you fix a width of */ +/* any number of cells, then size the remaining cells */ +/* using: */ +/* calc(<100% divided by number of non-fixed width cells> - ) */ + html.template-service.template-list td:first-child a span, html.template-node.template-show #services td:first-child a span { @extend %with-external-source-icon; @@ -27,6 +42,7 @@ html.template-node.template-show #services td:first-child a span { margin-right: 10px; margin-top: 2px; } +/*TODO: trs only live in tables, get rid of table */ html.template-service.template-list main table tr { @extend %services-row; } @@ -39,6 +55,21 @@ html.template-kv.template-list main table tr { html.template-acl.template-list main table tr { @extend %acls-row; } +html.template-policy.template-list main table tr { + @extend %policies-row; +} +html.template-token.template-list main table tr { + @extend %tokens-row; +} +html.template-policy.template-edit [role='dialog'] table tr, +html.template-policy.template-edit main table tr { + @extend %tokens-minimal-row; +} +html.template-token.template-list main table tr td.me, +html.template-token.template-list main table tr td.me ~ td, +html.template-token.template-list main table tr th { + @extend %tokens-your-row; +} html.template-node.template-show main table tr { @extend %node-services-row; } @@ -76,32 +107,68 @@ html.template-node.template-show main table.sessions tr { } } %intentions-row > * { - width: calc(25% - 60px); + width: calc(25% - 15px); } %intentions-row > *:last-child { - width: 60px; -} -%kvs-row > *:first-child { - width: calc(100% - 60px); -} -%kvs-row > *:last-child { - width: 60px; -} -%node-services-row > * { - width: 33%; + @extend %table-actions; } %acls-row > * { width: calc(50% - 30px); } %acls-row > *:last-child { - width: 60px; + @extend %table-actions; +} +%tokens-row > *:first-child, +%tokens-minimal-row > *:not(last-child), +%tokens-row > *:nth-child(2), +%tokens-your-row:nth-last-child(2) { + width: 120px; +} +%tokens-row > *:nth-child(3), +%tokens-row > *:nth-child(4) { + width: calc(50% - 150px); +} +%tokens-your-row:nth-child(4) { + width: calc(50% - 270px) !important; +} +%tokens-row > *:last-child { + @extend %table-actions; +} +%tokens-minimal-row > *:last-child { + width: calc(100% - 240px); +} +@media #{$--lt-medium-table} { + /* Token > Policies */ + /* Token > Your Token */ + html.template-token.template-list tr > :nth-child(2), + html.template-token.template-list tr > :nth-child(4), + html.template-token.template-list tr th:nth-child(5), + html.template-token.template-list main table tr td.me ~ td:nth-of-type(5) { + display: none; + } +} + +%kvs-row > *:first-child { + width: calc(100% - 60px); +} +%kvs-row > *:last-child { + @extend %table-actions; +} +%node-services-row > * { + width: 33%; +} +%policies-row > * { + width: calc(33% - 20px); +} +%policies-row > *:last-child { + @extend %table-actions; } // this will get auto calculated later in tabular-collection.js // keeping this here for reference -%services-row > * { - // (100% / 2) - (160px / 2) - // width: calc(50% - 160px); -} +// %services-row > * { +// (100% / 2) - (160px / 2) +// width: calc(50% - 160px); +// } %services-row > * { width: auto; } diff --git a/ui-v2/app/styles/components/tabular-details.scss b/ui-v2/app/styles/components/tabular-details.scss new file mode 100644 index 0000000000..457e1be3da --- /dev/null +++ b/ui-v2/app/styles/components/tabular-details.scss @@ -0,0 +1,4 @@ +@import './tabular-details/index'; +table.with-details { + @extend %tabular-details; +} diff --git a/ui-v2/app/styles/components/tabular-details/index.scss b/ui-v2/app/styles/components/tabular-details/index.scss new file mode 100644 index 0000000000..bc18252196 --- /dev/null +++ b/ui-v2/app/styles/components/tabular-details/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/styles/components/tabular-details/layout.scss b/ui-v2/app/styles/components/tabular-details/layout.scss new file mode 100644 index 0000000000..2f0a958fb2 --- /dev/null +++ b/ui-v2/app/styles/components/tabular-details/layout.scss @@ -0,0 +1,69 @@ +/* TODO: rename: %details-table */ +%tabular-details { + width: 100%; + table-layout: fixed; +} +%tabular-details tr > .actions { + @extend %table-actions; + position: relative; +} +%tabular-details td:only-child > div { + @extend %tabular-detail; +} +%tabular-details-toggle-button { + @extend %toggle-button; + pointer-events: auto; + position: absolute; +} +%tabular-details td > label { + @extend %tabular-details-toggle-button; + /*TODO: This needs to be figured out with %toggle-button/%action-group */ + top: 8px; + right: 15px; +} +%tabular-details tr:nth-child(even) td > * { + display: none; +} +%tabular-details tr:nth-child(odd) td { + width: calc(50% - 30px); +} +%tabular-details tr:nth-child(odd) td:last-child { + width: 60px; +} +%tabular-detail > label { + @extend %tabular-details-toggle-button; + top: 8px; + right: 24px; +} +%tabular-details tr:nth-child(even) td > input:checked + * { + display: block; +} +%tabular-details td:only-child { + overflow: visible; + position: relative; +} +%tabular-detail { + position: relative; + left: -10px; + right: -10px; + width: calc(100% + 20px); + margin-top: -48px; + pointer-events: none; + overflow: hidden; +} +%tabular-detail { + padding: 10px; +} +%tabular-detail::before { + content: ''; + display: block; + height: 1px; + position: absolute; + top: -2px; + left: 0; + width: 100%; +} +%tabular-detail > div { + pointer-events: auto; + margin-top: 36px; +} diff --git a/ui-v2/app/styles/components/tabular-details/skin.scss b/ui-v2/app/styles/components/tabular-details/skin.scss new file mode 100644 index 0000000000..f3b7f38b09 --- /dev/null +++ b/ui-v2/app/styles/components/tabular-details/skin.scss @@ -0,0 +1,20 @@ +%tabular-details-toggle-button { + @extend %with-chevron-down; +} +%tabular-details td:only-child { + cursor: default; +} +%tabular-detail { + border: 1px solid $ui-gray-300; + border-radius: $decor-radius-100; + box-shadow: 0 8px 10px 0 rgba($ui-black, 0.1); + margin-bottom: 20px; +} +%tabular-detail::before, +%tabular-detail > div, +%tabular-detail > label { + background-color: $ui-white; +} +%tabular-detail > label::before { + transform: rotate(180deg); +} diff --git a/ui-v2/app/styles/components/toggle-button.scss b/ui-v2/app/styles/components/toggle-button.scss new file mode 100644 index 0000000000..86d395e6f1 --- /dev/null +++ b/ui-v2/app/styles/components/toggle-button.scss @@ -0,0 +1 @@ +@import './toggle-button/index'; diff --git a/ui-v2/app/styles/components/toggle-button/index.scss b/ui-v2/app/styles/components/toggle-button/index.scss new file mode 100644 index 0000000000..07dc13fdcb --- /dev/null +++ b/ui-v2/app/styles/components/toggle-button/index.scss @@ -0,0 +1,2 @@ +@import './skin.scss'; +@import './layout.scss'; diff --git a/ui-v2/app/styles/components/toggle-button/layout.scss b/ui-v2/app/styles/components/toggle-button/layout.scss new file mode 100644 index 0000000000..9d0294d3a2 --- /dev/null +++ b/ui-v2/app/styles/components/toggle-button/layout.scss @@ -0,0 +1,11 @@ +%toggle-button { + width: 30px; + height: 30px; + /* center */ + display: flex; + align-items: center; + justify-content: center; +} +%toggle-button span { + display: none; +} diff --git a/ui-v2/app/styles/components/toggle-button/skin.scss b/ui-v2/app/styles/components/toggle-button/skin.scss new file mode 100644 index 0000000000..759b2e430f --- /dev/null +++ b/ui-v2/app/styles/components/toggle-button/skin.scss @@ -0,0 +1,11 @@ +%toggle-button { + border-radius: $radius-small; + cursor: pointer; +} +%toggle-button:hover, +%toggle-button:focus { + background-color: $ui-gray-050; +} +%toggle-button:active { + background-color: $ui-gray-100; +} diff --git a/ui-v2/app/styles/components/toggle/layout.scss b/ui-v2/app/styles/components/toggle/layout.scss index f91509412b..3a839b494b 100644 --- a/ui-v2/app/styles/components/toggle/layout.scss +++ b/ui-v2/app/styles/components/toggle/layout.scss @@ -1,33 +1,36 @@ -%toggle { +%toggle label { position: relative; } %toggle input { display: none; } -%toggle span { +%toggle label span { display: inline-block; - padding-left: 25px; + padding-left: 34px; } -%toggle span::before, -%toggle span::after { +%toggle label span::before, +%toggle label span::after { position: absolute; display: block; content: ''; top: 50%; } -%toggle span::before { +%toggle label span::before { left: 0px; - width: 20px; + width: 24px; height: 12px; - margin-top: -8px; + margin-top: -5px; } -%toggle span::after { - left: 2px; - margin-top: -6px; +%toggle label span::after { + margin-top: -3px; width: 8px; height: 8px; } -%toggle input:checked + span::after { - left: 10px; +%toggle label input:checked + span::after, +%toggle-negative label input + span::after { + left: 14px; +} +%toggle label span::after, +%toggle-negative label input:checked + span::after { + left: 2px; } - diff --git a/ui-v2/app/styles/components/toggle/skin.scss b/ui-v2/app/styles/components/toggle/skin.scss index 2127ec82b6..df2500819f 100644 --- a/ui-v2/app/styles/components/toggle/skin.scss +++ b/ui-v2/app/styles/components/toggle/skin.scss @@ -1,25 +1,30 @@ /* TODO: Maybe move this to reset? */ -[type='radio'], -%toggle span::before, -%toggle span::after { +%toggle label span { cursor: pointer; } -%toggle span::after { +%toggle label span::after { border-radius: $decor-radius-full; } -%toggle span::before { +%toggle label span::before { border-radius: 7px; } -%toggle input:checked + span::before { - background-color: $ui-color-success; +%toggle-negative { + border: 0; } -%toggle span { +%toggle.type-negative { + @extend %toggle-negative; +} +%toggle label span { color: $ui-gray-900; } -%toggle span::after { +%toggle label span::after { background-color: $ui-white; } -%toggle span::before { +%toggle label input:checked + span::before, +%toggle-negative label input + span::before { + background-color: $ui-blue-500; +} +%toggle label span::before, +%toggle-negative label input:checked + span::before { background-color: $ui-gray-300; } - diff --git a/ui-v2/app/styles/components/with-tooltip/layout.scss b/ui-v2/app/styles/components/with-tooltip/layout.scss index 7e16331c04..c3a3193dbd 100644 --- a/ui-v2/app/styles/components/with-tooltip/layout.scss +++ b/ui-v2/app/styles/components/with-tooltip/layout.scss @@ -7,6 +7,7 @@ %tooltip-bubble, %tooltip-tail { position: absolute; + z-index: 1; } %tooltip-bubble { padding: 10px; diff --git a/ui-v2/app/styles/core/layout.scss b/ui-v2/app/styles/core/layout.scss index 974d634474..e377fde83d 100644 --- a/ui-v2/app/styles/core/layout.scss +++ b/ui-v2/app/styles/core/layout.scss @@ -1,5 +1,13 @@ @import '../layouts/index'; +// TODO: This shouldn't be done here, decide the best way to do this +// %main-decoration ? %main-skin ? %content-skin ? +// it includes layouts of components, but not layout of itself? +// %main-components? What about %app-content +main, +%modal-window { + @extend %main-content; +} html.template-with-vertical-menu, html.template-with-vertical-menu body { overflow: hidden; @@ -33,8 +41,8 @@ html.template-edit main { } } /* TODO: keep margin below forms, move elsewhere */ -main form, -main form + div .with-confirmation { +%main-content form, +%main-content form + div .with-confirmation { margin-bottom: 2em; } @media #{$--lt-wide-form} { diff --git a/ui-v2/app/styles/core/typography.scss b/ui-v2/app/styles/core/typography.scss index e39a068e3d..4107ebf9f2 100644 --- a/ui-v2/app/styles/core/typography.scss +++ b/ui-v2/app/styles/core/typography.scss @@ -1,7 +1,8 @@ %button { font-family: $typo-family-sans; } -main p { +main p, +%modal-window p { margin-bottom: 1em; } %button, @@ -41,7 +42,9 @@ h2, td a { font-weight: $typo-weight-semibold; } -%form-element > span { +%form-element > span, +%toggle label span, +caption { font-weight: $typo-weight-semibold; } %button { @@ -57,6 +60,7 @@ th, main label a[rel*='help'], td:first-child em, %pill, +%form-element > strong, %healthchecked-resource strong { font-weight: $typo-weight-normal; } @@ -69,7 +73,7 @@ td:first-child em, font-size: inherit; } h1 { - font-size: $typo-size-400; + font-size: $typo-header-100; } h2, %header-drop-nav .is-active { @@ -83,13 +87,15 @@ td { font-size: $typo-size-600; } th, +caption, +.type-dialog, %form-element > span, %tooltip-bubble, %healthchecked-resource strong, %footer { font-size: $typo-size-700; } -%toggle span { +%toggle label span { font-size: $typo-size-700 !important; } %app-content > p:only-child, @@ -98,6 +104,7 @@ th, .template-error > div, %button, %form-element > em, +%form-element > strong, .with-confirmation p, %feedback-dialog-inline p { font-size: $typo-size-800; diff --git a/ui-v2/app/styles/layouts/containers.scss b/ui-v2/app/styles/layouts/containers.scss index 1f1d854c5e..4402761fef 100644 --- a/ui-v2/app/styles/layouts/containers.scss +++ b/ui-v2/app/styles/layouts/containers.scss @@ -17,6 +17,7 @@ $ideal-content-padding: 33px; margin-left: auto; margin-right: auto; } +%modal-dialog > *, %content-container > * { box-sizing: border-box; } diff --git a/ui-v2/app/styles/routes/dc/acls/index.scss b/ui-v2/app/styles/routes/dc/acls/index.scss index 7dc9536263..bf3fb279ba 100644 --- a/ui-v2/app/styles/routes/dc/acls/index.scss +++ b/ui-v2/app/styles/routes/dc/acls/index.scss @@ -1,3 +1,19 @@ +td a.is-management { + @extend %with-star-after; +} +td a.is-management::after { + height: 16px; + top: 2px; + padding-left: 32px; +} +@media #{$--horizontal-tabs} { + .template-policy.template-list main header .actions, + .template-token.template-list main header .actions { + position: relative; + top: 50px; + } +} + @media #{$--lt-wide-form} { html.template-acl.template-edit main header .actions { float: none; diff --git a/ui-v2/app/styles/routes/dc/acls/policies/index.scss b/ui-v2/app/styles/routes/dc/acls/policies/index.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui-v2/app/styles/routes/dc/acls/tokens/index.scss b/ui-v2/app/styles/routes/dc/acls/tokens/index.scss new file mode 100644 index 0000000000..0715d3371b --- /dev/null +++ b/ui-v2/app/styles/routes/dc/acls/tokens/index.scss @@ -0,0 +1,33 @@ +.template-token.template-edit [for='new-policy-toggle'] { + @extend %anchor; + cursor: pointer; + float: right; +} +%pill.policy-management { + @extend %with-star; +} +%token-yours { + text-indent: 20px; + color: $ui-blue-500; + padding-left: 15px; +} +%token-yours::after { + @extend %with-tick; + border-radius: 100%; + background-color: $ui-blue-500; +} +.me ~ :nth-last-child(2) { + @extend %token-yours; +} +.template-token.template-list main .notice { + margin-top: -20px; +} +.template-token.template-edit dd { + display: flex; +} +.template-token.template-edit dl { + @extend %form-row; +} +.template-token.template-edit dd .with-feedback { + top: -5px; +} diff --git a/ui-v2/app/styles/variables/custom-query.scss b/ui-v2/app/styles/variables/custom-query.scss index 84ea26f0d9..56895ef266 100644 --- a/ui-v2/app/styles/variables/custom-query.scss +++ b/ui-v2/app/styles/variables/custom-query.scss @@ -32,8 +32,13 @@ $--lt-spacious-healthcheck-status: '(max-width: 420px)'; $--wide-form: '(min-width: 421px)'; $--lt-wide-form: '(max-width: 420px)'; +/* If these are needed for usage on the same table */ +/* they will need re-figuring out */ $--wide-table: '(min-width: 421px)'; $--lt-wide-table: '(max-width: 420px)'; +$--medium-table: '(min-width: 850px)'; +$--lt-medium-table: '(max-width: 849px)'; +/* */ $--fixed-grid: '(min-width: 1260px)'; $--lt-fixed-grid: '(max-width: 1259px)'; diff --git a/ui-v2/app/templates/components/app-view.hbs b/ui-v2/app/templates/components/app-view.hbs index c0e5e56ace..52bcfc61fc 100644 --- a/ui-v2/app/templates/components/app-view.hbs +++ b/ui-v2/app/templates/components/app-view.hbs @@ -5,30 +5,43 @@ {{#flash-message flash=flash as |component flash|}} {{! flashes automatically ucfirst the type }} -

{{if (eq component.flashType 'Success') 'Success!' 'Error!'}} {{#yield-slot 'notification' (block-params (lowercase component.flashType) (lowercase flash.action) )}}{{yield}}{{/yield-slot}}

+

{{if (eq component.flashType 'Success') 'Success!' 'Error!'}} {{#yield-slot 'notification' (block-params (lowercase component.flashType) (lowercase flash.action) flash.item )}}{{yield}}{{/yield-slot}}

{{/flash-message}} {{/each}}
+ {{#if authorized}} {{#yield-slot 'actions'}}{{yield}}{{/yield-slot}} + {{/if}}
+
+ {{#if authorized}} + {{/if}} {{#yield-slot 'header'}}{{yield}}{{/yield-slot}}
+ {{#if authorized}} {{#yield-slot 'toolbar'}} {{yield}} {{/yield-slot}} + {{/if}} {{/if}}
- {{#if loading}} - {{partial 'consul-loading'}} +{{#if loading}} + {{partial 'consul-loading'}} +{{else}} + {{#if (not enabled) }} + {{#yield-slot 'disabled'}}{{yield}}{{/yield-slot}} + {{else if (not authorized)}} + {{#yield-slot 'authorization'}}{{yield}}{{/yield-slot}} {{else}} - {{#yield-slot 'content'}}{{yield}}{{/yield-slot}} + {{#yield-slot 'content'}}{{yield}}{{/yield-slot}} {{/if}} +{{/if}}
diff --git a/ui-v2/app/templates/components/code-editor.hbs b/ui-v2/app/templates/components/code-editor.hbs index ab4dc7844c..029c902cda 100644 --- a/ui-v2/app/templates/components/code-editor.hbs +++ b/ui-v2/app/templates/components/code-editor.hbs @@ -1,8 +1,7 @@ {{ivy-codemirror value=value - readonly=readonly name=name class=class - options=(hash lineNumbers=true mode=mode theme='hashi' showCursorWhenSelecting=true) + options=(hash readOnly=readonly lineNumbers=true mode=mode theme='hashi' showCursorWhenSelecting=true) valueUpdated=(action onkeyup) }} diff --git a/ui-v2/app/templates/components/confirmation-dialog.hbs b/ui-v2/app/templates/components/confirmation-dialog.hbs index 05602bba79..e2ce319f5d 100644 --- a/ui-v2/app/templates/components/confirmation-dialog.hbs +++ b/ui-v2/app/templates/components/confirmation-dialog.hbs @@ -1,11 +1,11 @@ {{yield}} -{{#if (or permanent (not confirming))}} {{#yield-slot 'action' (block-params confirm cancel)}} +{{#if (or permanent (not confirming))}} {{yield}} -{{/yield-slot}} {{/if}} -{{#if confirming }} -{{#yield-slot 'dialog' (block-params execute cancel message actionName)}} - {{yield}} {{/yield-slot}} -{{/if}} \ No newline at end of file +{{#yield-slot 'dialog' (block-params execute cancel message actionName)}} +{{#if confirming }} + {{yield}} +{{/if}} +{{/yield-slot}} \ No newline at end of file diff --git a/ui-v2/app/templates/components/copy-button-feedback.hbs b/ui-v2/app/templates/components/copy-button-feedback.hbs new file mode 100644 index 0000000000..8353e2f97a --- /dev/null +++ b/ui-v2/app/templates/components/copy-button-feedback.hbs @@ -0,0 +1,15 @@ +{{#feedback-dialog type='inline'}} + {{#block-slot 'action' as |success error|}} + {{#copy-button success=(action success) error=(action error) clipboardText=copy title=(concat 'Copy ' name ' to the clipboard')}}{{#if hasBlock }}{{yield}}{{else}}{{value}}{{/if}}{{/copy-button}} + {{/block-slot}} + {{#block-slot 'success' as |transition|}} +

+ Copied {{name}}! +

+ {{/block-slot}} + {{#block-slot 'error' as |transition|}} +

+ Sorry, something went wrong! +

+ {{/block-slot}} +{{/feedback-dialog}} diff --git a/ui-v2/app/templates/components/copy-button.hbs b/ui-v2/app/templates/components/copy-button.hbs new file mode 100644 index 0000000000..25b6ca9274 --- /dev/null +++ b/ui-v2/app/templates/components/copy-button.hbs @@ -0,0 +1,2 @@ +{{! this overriding template makes sure you can add button:empty's }} +{{~yield~}} diff --git a/ui-v2/app/templates/components/delete-confirmation.hbs b/ui-v2/app/templates/components/delete-confirmation.hbs new file mode 100644 index 0000000000..78bf85a6e2 --- /dev/null +++ b/ui-v2/app/templates/components/delete-confirmation.hbs @@ -0,0 +1,7 @@ +

+ {{ message }} +

+ + diff --git a/ui-v2/app/templates/components/dom-buffer-flush.hbs b/ui-v2/app/templates/components/dom-buffer-flush.hbs new file mode 100644 index 0000000000..fb5c4b157d --- /dev/null +++ b/ui-v2/app/templates/components/dom-buffer-flush.hbs @@ -0,0 +1 @@ +{{yield}} \ No newline at end of file diff --git a/ui-v2/app/templates/components/dom-buffer.hbs b/ui-v2/app/templates/components/dom-buffer.hbs new file mode 100644 index 0000000000..fb5c4b157d --- /dev/null +++ b/ui-v2/app/templates/components/dom-buffer.hbs @@ -0,0 +1 @@ +{{yield}} \ No newline at end of file diff --git a/ui-v2/app/templates/components/hashicorp-consul.hbs b/ui-v2/app/templates/components/hashicorp-consul.hbs index 9d746ce27f..916d69343c 100644 --- a/ui-v2/app/templates/components/hashicorp-consul.hbs +++ b/ui-v2/app/templates/components/hashicorp-consul.hbs @@ -31,7 +31,7 @@ Key/Value
  • - ACL + ACL
  • Intentions @@ -44,9 +44,11 @@
  • Documentation
  • +{{#if false }}
  • Settings
  • +{{/if}} @@ -59,4 +61,5 @@

    Consul {{env 'CONSUL_VERSION'}}

    Documentation {{{concat ''}}} - \ No newline at end of file + + {{modal-layer}} \ No newline at end of file diff --git a/ui-v2/app/templates/components/modal-dialog.hbs b/ui-v2/app/templates/components/modal-dialog.hbs new file mode 100644 index 0000000000..5ee2ba9e4b --- /dev/null +++ b/ui-v2/app/templates/components/modal-dialog.hbs @@ -0,0 +1,19 @@ +{{yield}} + +
    + +
    +
    +
    + + {{#yield-slot 'header'}}{{yield}}{{/yield-slot}} +
    +
    + {{#yield-slot 'body'}}{{yield}}{{/yield-slot}} +
    +
    + {{#yield-slot 'actions' (block-params (action 'close'))}}{{yield}}{{/yield-slot}} +
    +
    +
    +
    \ No newline at end of file diff --git a/ui-v2/app/templates/components/modal-layer.hbs b/ui-v2/app/templates/components/modal-layer.hbs new file mode 100644 index 0000000000..a76e982c1c --- /dev/null +++ b/ui-v2/app/templates/components/modal-layer.hbs @@ -0,0 +1 @@ + diff --git a/ui-v2/app/templates/components/secret-button.hbs b/ui-v2/app/templates/components/secret-button.hbs new file mode 100644 index 0000000000..ea4072a384 --- /dev/null +++ b/ui-v2/app/templates/components/secret-button.hbs @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/ui-v2/app/templates/components/tab-nav.hbs b/ui-v2/app/templates/components/tab-nav.hbs index f11a3ff39e..746f1f46c2 100644 --- a/ui-v2/app/templates/components/tab-nav.hbs +++ b/ui-v2/app/templates/components/tab-nav.hbs @@ -1,9 +1,13 @@ {{!