diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a8d54af..4747cf922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ If you're upgrading a previous API Umbrella version, you may upgrade the `api-um - **Route API backend requests directly from Traffic Server:** Routing to API backends has been simplified so it occurs directly from Traffic Server, instead of routing back through an extra nginx hop. This should improve efficiency, simplifies routing, and eliminates DNS-related code. ([#410](https://github.com/NREL/api-umbrella/pull/410)) - **Admin UI Upgrades:** Upgrade the admin UI project from Ember 2.8 to Ember 3.9 and Bootstrap 3 to Bootstrap 4. This switch also moves all dependencies into NPM instead of Bower, and better uses ES6 syntax throughout the admin UI code. Integration tests have also been switched from PhantomJS to Selenium tests using headless Chrome. ([#429](https://github.com/NREL/api-umbrella/pull/429), [api.data.gov#434](https://github.com/18F/api.data.gov/issues/434)) +- **Bootstrap upgrade:** Bootstrap upgrade for admin UI project from Bootstrap version 4 to 5.3 (02/2026) - **Upgrade to GeoIP2 database:** The legacy GeoIP data previously being used has been discontinued, so GeoIP2 is now being used for geo-locating IP addresses. ([8f17dae](https://github.com/NREL/api-umbrella/commit/8f17dae991de7553cb67ee319392bf53c17a5083), [#440](https://github.com/NREL/api-umbrella/issues/440)) - **Redirect all website content to HTTPS by default:** All website requests now redirect to HTTPS by default. ([b3a8abc](https://github.com/NREL/api-umbrella/commit/b3a8abc81347cbeb274e00f40fcfccea3af1b546), [#407](https://github.com/NREL/api-umbrella/issues/407), [api.data.gov#430](https://github.com/18F/api.data.gov/issues/430)) - **Improve HTTPS requirements for API requests to error earlier:** When making an insecure API request, return an error about HTTPS being required before the API key requirement error. ([api.data.gov#454](https://github.com/18F/api.data.gov/issues/454)) diff --git a/Dockerfile b/Dockerfile index 1940a7e84..b0b08ba8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -74,7 +74,8 @@ COPY tasks/app/example-website/build /app/tasks/app/example-website/ RUN make app:example-website:build && make clean:dev COPY src/api-umbrella/web-app/assets /app/src/api-umbrella/web-app/assets -COPY src/api-umbrella/web-app/webpack.config.js /app/src/api-umbrella/web-app/webpack.config.js +COPY src/api-umbrella/web-app/package.json /app/src/api-umbrella/web-app/package.json +COPY src/api-umbrella/web-app/vite.config.js /app/src/api-umbrella/web-app/vite.config.js COPY tasks/app/web-app/precompile /app/tasks/app/web-app/ RUN make app:web-app:precompile && make clean:dev diff --git a/Taskfile.yml b/Taskfile.yml index b49833049..b4c340f03 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -290,7 +290,8 @@ tasks: sources: - ./build/work/stamp/app-deps/web-app/pnpm - ./src/api-umbrella/web-app/assets/**/*.scss - - ./src/api-umbrella/web-app/webpack.config.js + - ./src/api-umbrella/web-app/package.json + - ./src/api-umbrella/web-app/vite.config.js - ./tasks/app/web-app/precompile - ./tasks/helpers.sh generates: diff --git a/docs/admin/api.rst b/docs/admin/api.rst index c54c9576f..445efe656 100644 --- a/docs/admin/api.rst +++ b/docs/admin/api.rst @@ -3,28 +3,28 @@ REST API .. raw:: html -
-
-
+
+
+
- - - - - - - - - - - - + + + + + + + + + + + + - + window.swaggerUi.load(); + diff --git a/src/api-umbrella/admin-ui/app/components/api-users/record-form.js b/src/api-umbrella/admin-ui/app/components/api-users/record-form.js index b11ad81dc..49b9ff963 100644 --- a/src/api-umbrella/admin-ui/app/components/api-users/record-form.js +++ b/src/api-umbrella/admin-ui/app/components/api-users/record-form.js @@ -72,20 +72,6 @@ export default class RecordForm extends Component.extend(Save) { return message; }, - messageHide(model) { - if(isNew && model.get('apiKey')) { - return false; - } else { - return true; - } - }, - messageWidth(model) { - if(isNew && model.get('apiKey')) { - return '500px'; - } else { - return undefined; - } - }, }); } } diff --git a/src/api-umbrella/admin-ui/app/components/apis/rewrite-table.js b/src/api-umbrella/admin-ui/app/components/apis/rewrite-table.js index 6b692005c..c018f0564 100644 --- a/src/api-umbrella/admin-ui/app/components/apis/rewrite-table.js +++ b/src/api-umbrella/admin-ui/app/components/apis/rewrite-table.js @@ -3,7 +3,6 @@ import Component from '@ember/component'; import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import { tagName } from '@ember-decorators/component'; -import Sortable from 'api-umbrella-admin-ui/utils/sortable'; import bootbox from 'bootbox'; import classic from 'ember-classic-decorator'; import without from 'lodash-es/without'; @@ -15,12 +14,6 @@ export default class RewriteTable extends Component { openModal = false; - init() { - super.init(...arguments); - - this.sortable = new Sortable(this.model.rewrites); - } - @action add() { this.set('rewriteModel', this.store.createRecord('api/rewrite')); diff --git a/src/api-umbrella/admin-ui/app/components/apis/sub-settings-table.js b/src/api-umbrella/admin-ui/app/components/apis/sub-settings-table.js index 50f7dec60..82f01a047 100644 --- a/src/api-umbrella/admin-ui/app/components/apis/sub-settings-table.js +++ b/src/api-umbrella/admin-ui/app/components/apis/sub-settings-table.js @@ -3,7 +3,6 @@ import Component from '@ember/component'; import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import { tagName } from '@ember-decorators/component'; -import Sortable from 'api-umbrella-admin-ui/utils/sortable'; import bootbox from 'bootbox'; import classic from 'ember-classic-decorator'; import without from 'lodash-es/without'; @@ -15,12 +14,6 @@ export default class SubSettingsTable extends Component { openModal = false; - init() { - super.init(...arguments); - - this.sortable = new Sortable(this.model.subSettings); - } - @action add() { this.set('subSettingsModel', this.store.createRecord('api/sub-settings')); diff --git a/src/api-umbrella/admin-ui/app/components/config/publish-form.js b/src/api-umbrella/admin-ui/app/components/config/publish-form.js index f5f36c8ae..5ce1bdf18 100644 --- a/src/api-umbrella/admin-ui/app/components/config/publish-form.js +++ b/src/api-umbrella/admin-ui/app/components/config/publish-form.js @@ -1,7 +1,7 @@ // eslint-disable-next-line ember/no-classic-components import Component from '@ember/component'; import { action, computed } from '@ember/object'; -import { success } from '@pnotify/core'; +import { service } from '@ember/service'; import LoadingButton from 'api-umbrella-admin-ui/utils/loading-button'; import bootbox from 'bootbox'; import Diff from 'diff'; @@ -12,6 +12,8 @@ import $ from 'jquery'; export default class PublishForm extends Component { tagName = ''; + @service pendingFlashMessages; + @action didInsert(element) { this.publishButton = element.querySelector('.publish-button'); @@ -121,10 +123,11 @@ export default class PublishForm extends Component { data: form.serialize(), }).then(() => { LoadingButton.reset(this.publishButton); - success({ + + this.pendingFlashMessages.add({ + type: 'success', title: 'Published', - text: 'Successfully published the configuration
Changes should be live in a few seconds...', - textTrusted: true, + message: 'Successfully published the configuration
Changes should be live in a few seconds...', }); this.refreshCurrentRouteController(); diff --git a/src/api-umbrella/admin-ui/app/components/flash-messages.js b/src/api-umbrella/admin-ui/app/components/flash-messages.js new file mode 100644 index 000000000..38ecfdfa7 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/flash-messages.js @@ -0,0 +1,6 @@ +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +export default class FlashMessages extends Component { + @service currentFlashMessages; +} diff --git a/src/api-umbrella/admin-ui/app/components/reorder-table.js b/src/api-umbrella/admin-ui/app/components/reorder-table.js new file mode 100644 index 000000000..2d992fa62 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/reorder-table.js @@ -0,0 +1,103 @@ +import {RestrictToVerticalAxis} from '@dnd-kit/abstract/modifiers'; +import {DragDropManager} from '@dnd-kit/dom'; +import {isSortable,Sortable} from '@dnd-kit/dom/sortable'; +import { action } from '@ember/object'; +import { guidFor } from '@ember/object/internals'; +import { schedule } from '@ember/runloop'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +export default class ReorderTable extends Component { + @tracked isReordering = false; + + constructor() { + super(...arguments); + this.setupDomSorting(); + } + + willDestroy() { + super.willDestroy(...arguments); + this.destroyDomSorting(); + } + + get isReorderable() { + const length = this.args.collection.length; + this.handleCollectionChange(); + return (length > 1); + } + + get container() { + const container = document.getElementById(this.args.tableId); + return container; + } + + handleCollectionChange() { + schedule('afterRender', this, function() { + this.setupDomSorting(); + }); + } + + @action + toggleReordering() { + this.isReordering = !this.isReordering; + this.setupDomSorting(); + } + + setupDomSorting() { + this.destroyDomSorting(); + + if(!this.container) { + console.error('tableId doesn not exist: ', this.args.tableId); + return; + } + + if(this.isReordering) { + this.container.classList.add('reorder-active'); + } else { + this.container.classList.remove('reorder-active'); + } + + this.manager = new DragDropManager({ + modifiers: (defaults) => [...defaults, RestrictToVerticalAxis], + }); + + this.sortables = []; + const tableRowEls = this.container.querySelectorAll('tbody tr'); + for(const [index, tableRowEl] of tableRowEls.entries()) { + this.sortables.push(new Sortable({ + id: tableRowEl.dataset.guid, + index, + element: tableRowEl, + handle: tableRowEl.querySelector('.reorder-handle'), + }, this.manager)); + } + + this.manager.monitor.addEventListener('dragend', this.handleDragEnd.bind(this)); + } + + destroyDomSorting() { + if(this.manager) { + this.manager.destroy(); + } + } + + handleDragEnd(event) { + if(event.canceled || !isSortable(event.operation.source)) { + return; + } + + const indexes = {}; + for(const sortable of this.sortables) { + indexes[sortable.id] = sortable.index; + } + + this.updateCollectionSortOrders(indexes); + } + + updateCollectionSortOrders(indexes) { + for(const record of this.args.collection) { + const index = indexes[guidFor(record)]; + record.set('sortOrder', index + 1); + } + } +} diff --git a/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-table.js b/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-table.js index 98f9f9981..d8a855cf0 100644 --- a/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-table.js +++ b/src/api-umbrella/admin-ui/app/components/stats/drilldown/results-table.js @@ -30,13 +30,13 @@ export default class ResultsTable extends Component { render: (name, type, data) => { if(type === 'display' && name && name !== '-') { if(data.terminal) { - return '' + escape(name); + return '' + escape(name); } else { let params = clone(this.presentQueryParamValues); params.prefix = data.descendent_prefix; let link = '#/stats/drilldown?' + $.param(params); - return '' + escape(name) + ''; + return '' + escape(name) + ''; } } diff --git a/src/api-umbrella/admin-ui/app/components/stats/logs/results-table.js b/src/api-umbrella/admin-ui/app/components/stats/logs/results-table.js index d2e689c65..fc927a78a 100644 --- a/src/api-umbrella/admin-ui/app/components/stats/logs/results-table.js +++ b/src/api-umbrella/admin-ui/app/components/stats/logs/results-table.js @@ -51,6 +51,7 @@ export default class ResultsTable extends Component { theme: 'light-border forced-wide', arrow: true, delay: 200, + appendTo: $cell[0], }); } }); @@ -330,7 +331,7 @@ export default class ResultsTable extends Component { const tooltipButtonEl = document.createElement('button'); tooltipButtonEl.className = 'btn btn-link btn-tooltip'; tooltipButtonEl.type = 'button'; - tooltipButtonEl.innerHTML = 'Help'; + tooltipButtonEl.innerHTML = 'Help'; tippy(tooltipButtonEl, { trigger: 'click', diff --git a/src/api-umbrella/admin-ui/app/components/stats/query-form.js b/src/api-umbrella/admin-ui/app/components/stats/query-form.js index d1b151e6c..47079fb4d 100644 --- a/src/api-umbrella/admin-ui/app/components/stats/query-form.js +++ b/src/api-umbrella/admin-ui/app/components/stats/query-form.js @@ -14,6 +14,7 @@ import QueryBuilder from 'jQuery-QueryBuilder'; import forEach from 'lodash-es/forEach'; import { marked } from 'marked'; import moment from 'moment-timezone'; +import tippy from 'tippy.js'; marked.use({ gfm: true, @@ -31,7 +32,7 @@ QueryBuilder.define('filter-description', function() { if(!buttonEl) { buttonEl = document.createElement('button'); buttonEl.type = 'button'; - buttonEl.className = 'btn btn-sm btn-info filter-description btn-tooltip tooltip-trigger'; + buttonEl.className = 'btn btn-sm btn-link border filter-description tooltip-trigger'; buttonEl.innerHTML = ''; const ruleActionEl = rule.$el[0].querySelector(QueryBuilder.selectors.rule_actions); @@ -474,6 +475,32 @@ export default class QueryForm extends Component { } else if(this.search) { this.send('toggleFilterType', 'advanced'); } + + const helpTriggerLink = document.querySelector('.lucene-help-link'); + const helpContent = document.getElementById('query_syntax_help_content'); + if(helpTriggerLink && helpContent) { + this.helpTippy = tippy(helpTriggerLink, { + trigger: 'click', + interactive: true, + theme: 'light-border', + arrow: true, + allowHTML: true, + content: helpContent.innerHTML, + placement: 'bottom-start', + maxWidth: 'none', + // Below the sticky navigation header, since this popover can be so + // large and cause the page to scroll. + zIndex: 1019, + }); + + } + } + + willDestroy() { + if(this.helpTippy) { + this.helpTippy.destroy(); + } + super.willDestroy(...arguments); } // eslint-disable-next-line ember/no-observers diff --git a/src/api-umbrella/admin-ui/app/initializers/fontawesome.js b/src/api-umbrella/admin-ui/app/initializers/fontawesome.js index 1ab119e22..abb875085 100644 --- a/src/api-umbrella/admin-ui/app/initializers/fontawesome.js +++ b/src/api-umbrella/admin-ui/app/initializers/fontawesome.js @@ -1,4 +1,67 @@ -import { dom } from '@fortawesome/fontawesome-svg-core'; +import '@fortawesome/fontawesome-svg-core/styles.css'; + +import { config, dom, library } from '@fortawesome/fontawesome-svg-core'; +import { + faFile, + faFolder, +} from '@fortawesome/free-regular-svg-icons'; +import { + faArrowDown, + faArrowRight, + faBars, + faCalendar, + faCaretDown, + faCog, + faGrip, + faGripVertical, + faLock, + faMapMarkerAlt, + faPencilAlt, + faPlus, + faPlusCircle, + faQuestionCircle, + faSignal, + faSitemap, + faSort, + faSortDown, + faSortUp, + faSyncAlt, + faTimes, + faUpload, + faUser, + faUsers, +} from '@fortawesome/free-solid-svg-icons'; + +config.autoAddCss = false; + +library.add( + faArrowDown, + faArrowRight, + faBars, + faCalendar, + faCaretDown, + faCog, + faFile, + faFolder, + faGrip, + faGripVertical, + faLock, + faMapMarkerAlt, + faPencilAlt, + faPlus, + faPlusCircle, + faQuestionCircle, + faSignal, + faSitemap, + faSort, + faSortDown, + faSortUp, + faSyncAlt, + faTimes, + faUpload, + faUser, + faUsers, +); export function initialize() { dom.watch(); diff --git a/src/api-umbrella/admin-ui/app/initializers/pnotify.js b/src/api-umbrella/admin-ui/app/initializers/pnotify.js deleted file mode 100644 index 9482417fa..000000000 --- a/src/api-umbrella/admin-ui/app/initializers/pnotify.js +++ /dev/null @@ -1,21 +0,0 @@ -import * as PNotifyBootstrap4 from '@pnotify/bootstrap4'; -import { defaultModules, defaults } from '@pnotify/core'; -import * as PNotifyFontAwesome5 from '@pnotify/font-awesome5'; -import * as PNotifyFontAwesome5Fix from '@pnotify/font-awesome5-fix'; -import * as PNotifyMobile from '@pnotify/mobile'; - -export function initialize() { - defaults.width = '400px'; - defaults.icon = false; - defaults.animation = 'none'; - defaults.sticker = false; - defaultModules.set(PNotifyMobile, {}); - defaultModules.set(PNotifyBootstrap4, {}); - defaultModules.set(PNotifyFontAwesome5Fix, {}); - defaultModules.set(PNotifyFontAwesome5, {}); -} - -export default { - name: 'pnotify', - initialize, -}; diff --git a/src/api-umbrella/admin-ui/app/initializers/test-pnotify.js b/src/api-umbrella/admin-ui/app/initializers/test-pnotify.js deleted file mode 100644 index a35848efa..000000000 --- a/src/api-umbrella/admin-ui/app/initializers/test-pnotify.js +++ /dev/null @@ -1,17 +0,0 @@ -import { defaultStack } from '@pnotify/core'; - -import config from '../config/environment'; - -export function initialize() { - if(config.integrationTestMode === true) { - // Export the removeAll function as a global, for use in our test suite. - window.PNotifyRemoveAll = function() { - defaultStack.close(); - } - } -} - -export default { - name: 'test-pnotify', - initialize, -}; diff --git a/src/api-umbrella/admin-ui/app/instance-initializers/datatables.js b/src/api-umbrella/admin-ui/app/instance-initializers/datatables.js index 998e53d98..3114b7a16 100644 --- a/src/api-umbrella/admin-ui/app/instance-initializers/datatables.js +++ b/src/api-umbrella/admin-ui/app/instance-initializers/datatables.js @@ -1,5 +1,5 @@ import 'datatables.net'; -import 'datatables.net-bs4'; +import 'datatables.net-bs5'; import $ from 'jquery'; import merge from 'lodash-es/merge'; @@ -55,7 +55,7 @@ export function initialize(appInstance) { merge($.fn.DataTable.ext.classes, { sFilterInput: 'form-control', - sLengthSelect: 'custom-select form-control', + sLengthSelect: 'form-select form-control', }); } diff --git a/src/api-umbrella/admin-ui/app/mixins/save.js b/src/api-umbrella/admin-ui/app/mixins/save.js index ba435ca60..a13e0a1f3 100644 --- a/src/api-umbrella/admin-ui/app/mixins/save.js +++ b/src/api-umbrella/admin-ui/app/mixins/save.js @@ -1,6 +1,5 @@ import Mixin from '@ember/object/mixin' -import { inject } from '@ember/service'; -import { success } from '@pnotify/core'; +import { inject as service } from '@ember/service'; import LoadingButton from 'api-umbrella-admin-ui/utils/loading-button'; import bootbox from 'bootbox'; import scrollTo from 'jquery.scrollto'; @@ -8,7 +7,9 @@ import isFunction from 'lodash-es/isFunction'; // eslint-disable-next-line ember/no-new-mixins export default Mixin.create({ - router: inject(), + router: service(), + + pendingFlashMessages: service(), scrollToErrors(button) { LoadingButton.reset(button); @@ -17,12 +18,10 @@ export default Mixin.create({ afterSaveComplete(options, button) { LoadingButton.reset(button); - success({ - title: 'Saved', - text: (isFunction(options.message)) ? options.message(this.model) : options.message, - hide: (isFunction(options.messageHide)) ? options.messageHide(this.model) : options.messageHide, - width: (isFunction(options.messageWidth)) ? options.messageWidth(this.model) : options.messageWidth, - textTrusted: true, + + this.pendingFlashMessages.add({ + type: 'success', + message: (isFunction(options.message)) ? options.message(this.model) : options.message, }); this.router.transitionTo(options.transitionToRoute); @@ -72,10 +71,10 @@ export default Mixin.create({ bootbox.confirm(options.prompt, (result) => { if(result) { this.model.destroyRecord().then(() => { - success({ + this.pendingFlashMessages.add({ + type: 'success', title: 'Deleted', - text: (isFunction(options.message)) ? options.message(this.model) : options.message, - textTrusted: true, + message: (isFunction(options.message)) ? options.message(this.model) : options.message, }); this.router.transitionTo(options.transitionToRoute); diff --git a/src/api-umbrella/admin-ui/app/routes/admin-groups/base.js b/src/api-umbrella/admin-ui/app/routes/admin-groups/base.js index 78b124819..f8cb1946c 100644 --- a/src/api-umbrella/admin-ui/app/routes/admin-groups/base.js +++ b/src/api-umbrella/admin-ui/app/routes/admin-groups/base.js @@ -5,7 +5,7 @@ export default class BaseRoute extends AuthenticatedRoute { setupController(controller, model) { controller.set('model', model); - $('ul.navbar-nav li').removeClass('active'); - $('ul.navbar-nav li.nav-users').addClass('active'); + $('ul.navbar-nav a.nav-link').removeClass('active'); + $('ul.navbar-nav li.nav-users > a.nav-link').addClass('active'); } } diff --git a/src/api-umbrella/admin-ui/app/routes/admins/base.js b/src/api-umbrella/admin-ui/app/routes/admins/base.js index 78b124819..802119bbd 100644 --- a/src/api-umbrella/admin-ui/app/routes/admins/base.js +++ b/src/api-umbrella/admin-ui/app/routes/admins/base.js @@ -5,7 +5,9 @@ export default class BaseRoute extends AuthenticatedRoute { setupController(controller, model) { controller.set('model', model); - $('ul.navbar-nav li').removeClass('active'); - $('ul.navbar-nav li.nav-users').addClass('active'); + $('ul.navbar-nav a.nav-link').removeClass('active'); + $('ul.navbar-nav li.nav-users > a.nav-link').addClass('active'); } } + + diff --git a/src/api-umbrella/admin-ui/app/routes/api-scopes/base.js b/src/api-umbrella/admin-ui/app/routes/api-scopes/base.js index 78b124819..f8cb1946c 100644 --- a/src/api-umbrella/admin-ui/app/routes/api-scopes/base.js +++ b/src/api-umbrella/admin-ui/app/routes/api-scopes/base.js @@ -5,7 +5,7 @@ export default class BaseRoute extends AuthenticatedRoute { setupController(controller, model) { controller.set('model', model); - $('ul.navbar-nav li').removeClass('active'); - $('ul.navbar-nav li.nav-users').addClass('active'); + $('ul.navbar-nav a.nav-link').removeClass('active'); + $('ul.navbar-nav li.nav-users > a.nav-link').addClass('active'); } } diff --git a/src/api-umbrella/admin-ui/app/routes/api-users/base.js b/src/api-umbrella/admin-ui/app/routes/api-users/base.js index 78b124819..f8cb1946c 100644 --- a/src/api-umbrella/admin-ui/app/routes/api-users/base.js +++ b/src/api-umbrella/admin-ui/app/routes/api-users/base.js @@ -5,7 +5,7 @@ export default class BaseRoute extends AuthenticatedRoute { setupController(controller, model) { controller.set('model', model); - $('ul.navbar-nav li').removeClass('active'); - $('ul.navbar-nav li.nav-users').addClass('active'); + $('ul.navbar-nav a.nav-link').removeClass('active'); + $('ul.navbar-nav li.nav-users > a.nav-link').addClass('active'); } } diff --git a/src/api-umbrella/admin-ui/app/routes/apis/base.js b/src/api-umbrella/admin-ui/app/routes/apis/base.js index f30ec7e1a..3819d0407 100644 --- a/src/api-umbrella/admin-ui/app/routes/apis/base.js +++ b/src/api-umbrella/admin-ui/app/routes/apis/base.js @@ -5,7 +5,7 @@ export default class BaseRoute extends AuthenticatedRoute { setupController(controller, model) { controller.set('model', model); - $('ul.navbar-nav li').removeClass('active'); - $('ul.navbar-nav li.nav-config').addClass('active'); + $('ul.navbar-nav a.nav-link').removeClass('active'); + $('ul.navbar-nav li.nav-config > a.nav-link').addClass('active'); } } diff --git a/src/api-umbrella/admin-ui/app/routes/application.js b/src/api-umbrella/admin-ui/app/routes/application.js index aad400b93..7a412c054 100644 --- a/src/api-umbrella/admin-ui/app/routes/application.js +++ b/src/api-umbrella/admin-ui/app/routes/application.js @@ -9,6 +9,9 @@ export default class ApplicationRoute extends Route { @service busy; + @service currentFlashMessages; + @service pendingFlashMessages; + async beforeModel() { await this.session.setup(); } @@ -59,6 +62,7 @@ export default class ApplicationRoute extends Route { @action refreshCurrentRoute() { this.refresh(); + window.scrollTo(0, 0); } @action @@ -79,4 +83,25 @@ export default class ApplicationRoute extends Route { return this.intermediateTransitionTo('error'); } } + + @action + didTransition() { + // On each route change, clear any currently visible flash messages, since + // we only want the flash messages to be visible on one page, but not + // persistent otherwise. + this.currentFlashMessages.empty(); + + // Next, copy any "pending" flash messages that were set just before this + // route change to the "current" messages to display. In this way, the + // "pending" changes can be shown just once. + for(const item of this.pendingFlashMessages.items) { + this.currentFlashMessages.add(item); + } + + // Clear the pending messages that have now been copied for one-time + // display to the current area. + this.pendingFlashMessages.empty(); + + return true; + } } diff --git a/src/api-umbrella/admin-ui/app/routes/stats/base.js b/src/api-umbrella/admin-ui/app/routes/stats/base.js index ca473a96d..e379abda3 100644 --- a/src/api-umbrella/admin-ui/app/routes/stats/base.js +++ b/src/api-umbrella/admin-ui/app/routes/stats/base.js @@ -16,8 +16,8 @@ export default class BaseRoute extends AuthenticatedRoute { controller.set('allQueryParamValues', this.allQueryParamValues || {}); controller.set('backendQueryParamValues', this.backendQueryParamValues || {}); - $('ul.navbar-nav li').removeClass('active'); - $('ul.navbar-nav li.nav-analytics').addClass('active'); + $('ul.navbar-nav a.nav-link').removeClass('active'); + $('ul.navbar-nav li.nav-analytics > a.nav-link').addClass('active'); } beforeModel() { diff --git a/src/api-umbrella/admin-ui/app/routes/website-backends/base.js b/src/api-umbrella/admin-ui/app/routes/website-backends/base.js index f30ec7e1a..3819d0407 100644 --- a/src/api-umbrella/admin-ui/app/routes/website-backends/base.js +++ b/src/api-umbrella/admin-ui/app/routes/website-backends/base.js @@ -5,7 +5,7 @@ export default class BaseRoute extends AuthenticatedRoute { setupController(controller, model) { controller.set('model', model); - $('ul.navbar-nav li').removeClass('active'); - $('ul.navbar-nav li.nav-config').addClass('active'); + $('ul.navbar-nav a.nav-link').removeClass('active'); + $('ul.navbar-nav li.nav-config > a.nav-link').addClass('active'); } } diff --git a/src/api-umbrella/admin-ui/app/services/current-flash-messages.js b/src/api-umbrella/admin-ui/app/services/current-flash-messages.js new file mode 100644 index 000000000..d645cb11b --- /dev/null +++ b/src/api-umbrella/admin-ui/app/services/current-flash-messages.js @@ -0,0 +1,4 @@ +import FlashMessagesService from './flash-messages'; + +export default class CurrentFlashMessagesService extends FlashMessagesService { +} diff --git a/src/api-umbrella/admin-ui/app/services/flash-messages.js b/src/api-umbrella/admin-ui/app/services/flash-messages.js new file mode 100644 index 000000000..99cea72bf --- /dev/null +++ b/src/api-umbrella/admin-ui/app/services/flash-messages.js @@ -0,0 +1,14 @@ +import { A } from '@ember/array'; +import Service from '@ember/service'; + +export default class FlashMessagesService extends Service { + items = A([]); + + add(item) { + this.items.pushObject(item); + } + + empty() { + this.items.clear(); + } +} diff --git a/src/api-umbrella/admin-ui/app/services/pending-flash-messages.js b/src/api-umbrella/admin-ui/app/services/pending-flash-messages.js new file mode 100644 index 000000000..b03f89cdc --- /dev/null +++ b/src/api-umbrella/admin-ui/app/services/pending-flash-messages.js @@ -0,0 +1,4 @@ +import FlashMessagesService from './flash-messages'; + +export default class PendingFlashMessagesService extends FlashMessagesService { +} diff --git a/src/api-umbrella/admin-ui/app/styles/_base.scss b/src/api-umbrella/admin-ui/app/styles/_base.scss index 42e599c51..7724cac1f 100644 --- a/src/api-umbrella/admin-ui/app/styles/_base.scss +++ b/src/api-umbrella/admin-ui/app/styles/_base.scss @@ -1,10 +1,6 @@ -body { - padding-top: 66px; -} - .code-block, .tippy-box pre { - background-color: $light; + background-color: $gray-100; border: $card-border-width solid $card-border-color; @include border-radius($card-border-radius); padding: 0.5rem; @@ -14,12 +10,8 @@ body { font-weight: bold; } -.button-actions { +.table-button-actions { text-align: right; - margin-bottom: 16px; -} - -.button-actions-down { position: relative; height: 0px; margin: 0px; @@ -27,11 +19,11 @@ body { } .arrow { - margin-top: 24px; + margin-top: 1.5rem; text-align: center; color: $gray-600; - font-size: 12px; - line-height: 12px; + font-size: 0.75rem; + line-height: 1em; .svg-inline--fa { color: lighten($gray-600, 16%); @@ -48,35 +40,21 @@ body { } a.remove-action { - font-size: 12px; - vertical-align: middle; + font-size: 0.75rem; color: $danger; } -a:hover { - cursor: pointer; -} - .api-key { font-family: $font-family-monospace; } .record-details { text-align: right; - font-size: 11px; - line-height: 13px; + font-size: 0.75rem; + line-height: 1.2em; color: $text-muted; } -.query-syntax-help .example { - text-align: right; -} - -.query-syntax-help td.example { - font-style: italic; - font-size: 11px; -} - .diff { del { text-decoration: none; @@ -89,19 +67,11 @@ a:hover { } } -.dropdown-menu { - .fa { - /* Make the icons a consistent width so the menu text lines up */ - width: 16px; - text-align: center; - } -} - .publish-toggle-checkboxes { - margin: -28px 0px 32px 17px; + margin: -1rem 0px 2rem 0px; text-align: center; - width: 80px; - font-size: 85%; + width: 5rem; + font-size: 0.75rem; } .control-group-negative-top { @@ -114,7 +84,7 @@ a:hover { margin-top: 3em; margin-bottom: 1em; padding-top: 1em; - font-size: 11px; + font-size: 0.75rem; color: $text-muted; } @@ -133,12 +103,7 @@ a:hover { } .btn-light { - @include button-variant($gray-300, $gray-300); -} - -.btn-link:not(:disabled):not(.disabled):active, -.btn-link:not(:disabled):not(.disabled).active { - box-shadow: none; + @include button-variant($gray-300, $gray-400); } // Workaround for ember-bootstrap 4.9.0 flashing modal backgrounds as solid @@ -148,10 +113,6 @@ a:hover { opacity: $modal-backdrop-opacity; } -.ui-pnotify-text p:last-of-type { +.alert p:last-of-type { margin-bottom: 0px; } - -.alert a { - text-decoration: underline; -} diff --git a/src/api-umbrella/admin-ui/app/styles/_forms.scss b/src/api-umbrella/admin-ui/app/styles/_forms.scss index 6da2ef368..07742f3ac 100644 --- a/src/api-umbrella/admin-ui/app/styles/_forms.scss +++ b/src/api-umbrella/admin-ui/app/styles/_forms.scss @@ -3,7 +3,6 @@ label { margin-bottom: 0.1rem; } -.custom-control-label, .form-check-label { font-weight: normal; } @@ -16,25 +15,29 @@ label { padding-top: 0px; } +.form-group { + margin-bottom: 1rem; +} + .form-horizontal.condensed .form-group { - margin-bottom: 5px; + margin-bottom: 0.25rem; } fieldset { - margin: 12px 0px 32px 0px; - padding: 6px 0px; + margin: 0.5rem 0px 2rem 0px; + padding: 0.25rem 0px; } fieldset.collapsible { - margin-bottom: 12px; + margin-bottom: 0.75rem; - .collapse.in { - margin-bottom: 24px; + .collapse.show { + margin-bottom: 2rem; } } legend { - margin-bottom: 2px; + margin-bottom: 0.5rem; border-width: 0px 0px 1px 0px; border-style: solid; border-color: $gray-600; @@ -54,26 +57,21 @@ legend a, legend button.btn-link { display: block; color: $link-color !important; -} - -legend a:hover, -legend button.btn-link:hover { - color: $link-hover-color !important; - text-decoration: none; -} + text-decoration: none !important; -legend button.btn-link:focus { - text-decoration: none; + &:hover { + color: $link-hover-color !important; + } } .fieldset-note { - font-size: 85%; + font-size: 0.85rem; color: $text-muted; - margin-bottom: 4px; + margin-bottom: 0.25rem; } fieldset .table { - margin-bottom: 4px; + margin-bottom: 0.25rem; } .has-error { @@ -89,7 +87,7 @@ fieldset .table { .error-messages { h4 { - margin-bottom: 4px; + margin-bottom: 0.25rem; } ul { @@ -121,27 +119,12 @@ fieldset .table { margin-top: 20px; } -.custom-control.custom-control-no-label { - display: inline-block; - padding-left: 1rem; - - .custom-control-label { - display: block; - width: 0px; - } - - .custom-control-label::before, - .custom-control-label::after { - left: -1rem; - } -} - .btn { .btn-loading-label { display: none; .svg-inline--fa:first-child { - @extend .mr-2; + @extend .me-2; } } @@ -162,3 +145,7 @@ fieldset .table { margin-bottom: 0px; } } + +.btn-light { + --bs-btn-active-bg: #{$gray-500}; +} diff --git a/src/api-umbrella/admin-ui/app/styles/_icons.scss b/src/api-umbrella/admin-ui/app/styles/_icons.scss index b7ca43351..742e92f28 100644 --- a/src/api-umbrella/admin-ui/app/styles/_icons.scss +++ b/src/api-umbrella/admin-ui/app/styles/_icons.scss @@ -1,7 +1,7 @@ a .svg-inline--fa:first-child { - @extend .mr-1; + @extend .me-1; } -[data-toggle='collapse'].collapsed .fa-caret-down { +[data-bs-toggle='collapse'].collapsed .fa-caret-down { transform: rotate(270deg); } diff --git a/src/api-umbrella/admin-ui/app/styles/_query-builder.scss b/src/api-umbrella/admin-ui/app/styles/_query-builder.scss index 54db47e2f..e6e4e7778 100644 --- a/src/api-umbrella/admin-ui/app/styles/_query-builder.scss +++ b/src/api-umbrella/admin-ui/app/styles/_query-builder.scss @@ -2,7 +2,7 @@ .query-builder { .pull-right { - float: right !important; + @extend .float-end; } .rules-group-container { @@ -43,9 +43,9 @@ } } - select.form-control { - @extend .custom-select; - @extend .custom-select-sm; + select.form-select { + @extend .form-select; + @extend .form-select-sm; } input[type='number'].form-control, diff --git a/src/api-umbrella/admin-ui/app/styles/_tables.scss b/src/api-umbrella/admin-ui/app/styles/_tables.scss index a7dd9f5ca..9721701c2 100644 --- a/src/api-umbrella/admin-ui/app/styles/_tables.scss +++ b/src/api-umbrella/admin-ui/app/styles/_tables.scss @@ -1,4 +1,4 @@ -@import "node_modules/datatables.net-bs4/css/dataTables.bootstrap4"; +@import "node_modules/datatables.net-bs5/css/dataTables.bootstrap5"; .table-nowrap th, .table-nowrap td { @@ -10,7 +10,7 @@ table.dataTable.table thead .sorting:after, table.dataTable.table thead .sorting_asc:before, table.dataTable.table thead .sorting_asc:after, table.dataTable.table thead .sorting_desc:before, -table.dataTable.table thead .sorting_desc:after, { +table.dataTable.table thead .sorting_desc:after { display: none; } @@ -19,7 +19,7 @@ table.dataTable.table thead .sorting_asc, table.dataTable.table thead .sorting_desc { .svg-inline--fa.fa-sort, .svg-inline--fa.fa-sort-up, - .svg-inline--fa.fa-sort-down, { + .svg-inline--fa.fa-sort-down { color: $gray-500; position: absolute; right: 0.5rem; @@ -31,7 +31,7 @@ table.dataTable.table thead .sorting_asc, table.dataTable.table thead .sorting_desc { .svg-inline--fa.fa-sort, .svg-inline--fa.fa-sort-up, - .svg-inline--fa.fa-sort-down, { + .svg-inline--fa.fa-sort-down { color: $link-color; } } @@ -39,7 +39,7 @@ table.dataTable.table thead .sorting_desc { table.dataTable.table thead .sorting_disabled { .svg-inline--fa.fa-sort, .svg-inline--fa.fa-sort-up, - .svg-inline--fa.fa-sort-down, { + .svg-inline--fa.fa-sort-down { display: none; } } @@ -88,7 +88,6 @@ div.dataTables_wrapper div.dataTables_paginate ul.pagination { div.dataTables_info { padding: 0px; font-size: 13px; - //color: $text-color-light; } div.dataTables_paginate { @@ -102,7 +101,6 @@ div.dataTables_length { label { float: none; font-size: 13px; - //color: $text-color-light; } select { diff --git a/src/api-umbrella/admin-ui/app/styles/_tooltips.scss b/src/api-umbrella/admin-ui/app/styles/_tooltips.scss index 2c7e7c0a2..679669f4f 100644 --- a/src/api-umbrella/admin-ui/app/styles/_tooltips.scss +++ b/src/api-umbrella/admin-ui/app/styles/_tooltips.scss @@ -10,7 +10,7 @@ } .col-form-label .btn-link.btn-tooltip { - margin-left: 8px; + margin-left: 0.5rem; } .tippy-box { diff --git a/src/api-umbrella/admin-ui/app/styles/_variables.scss b/src/api-umbrella/admin-ui/app/styles/_variables.scss index 3dbaa4a23..92ddebbf8 100644 --- a/src/api-umbrella/admin-ui/app/styles/_variables.scss +++ b/src/api-umbrella/admin-ui/app/styles/_variables.scss @@ -3,4 +3,10 @@ $enable-shadows: true; // Make placeholder text lighter, or else it's hard to distinguish it between // text that's filled in: https://github.com/twbs/bootstrap/issues/21624 This // new color is based on the browser defaults. -$input-placeholder-color: #a9a9a9; +// $input-placeholder-color: #a9a9a9; +$input-placeholder-color: #86898b; +$border-color: #adb5bd; + +$table-border-color: #ced4da; + +$light: #dee2e6; diff --git a/src/api-umbrella/admin-ui/app/styles/app.scss b/src/api-umbrella/admin-ui/app/styles/app.scss index 076670770..51e7f59a1 100644 --- a/src/api-umbrella/admin-ui/app/styles/app.scss +++ b/src/api-umbrella/admin-ui/app/styles/app.scss @@ -1,8 +1,5 @@ @import "_variables"; @import "node_modules/bootstrap/scss/bootstrap"; -@import "node_modules/@pnotify/core/dist/PNotify"; -@import "node_modules/@pnotify/mobile/dist/PNotifyMobile"; -@import "node_modules/@pnotify/bootstrap4/dist/PNotifyBootstrap4"; @import "_tooltips"; @import "_query-builder"; diff --git a/src/api-umbrella/admin-ui/app/templates/admin-groups/index.hbs b/src/api-umbrella/admin-ui/app/templates/admin-groups/index.hbs index f6648e405..38ba45314 100644 --- a/src/api-umbrella/admin-ui/app/templates/admin-groups/index.hbs +++ b/src/api-umbrella/admin-ui/app/templates/admin-groups/index.hbs @@ -1,11 +1,11 @@

Admin Groups

{{#if this.currentAdmin.permissions.admin_manage}} -
+
Add New Admin Group
{{else}} -
Contact us to make adminstrator changes.
+ {{/if}} diff --git a/src/api-umbrella/admin-ui/app/templates/admins/index.hbs b/src/api-umbrella/admin-ui/app/templates/admins/index.hbs index 49f37fe27..7893617fe 100644 --- a/src/api-umbrella/admin-ui/app/templates/admins/index.hbs +++ b/src/api-umbrella/admin-ui/app/templates/admins/index.hbs @@ -1,11 +1,11 @@

Admins

{{#if this.currentAdmin.permissions.admin_manage}} -
+
Add New Admin
{{else}} -
Contact us to make adminstrator changes.
+ {{/if}} diff --git a/src/api-umbrella/admin-ui/app/templates/api-scopes/index.hbs b/src/api-umbrella/admin-ui/app/templates/api-scopes/index.hbs index 272306a9d..b28739d81 100644 --- a/src/api-umbrella/admin-ui/app/templates/api-scopes/index.hbs +++ b/src/api-umbrella/admin-ui/app/templates/api-scopes/index.hbs @@ -1,11 +1,11 @@

API Scopes

{{#if this.currentAdmin.permissions.admin_manage}} -
+
Add New API Scope
{{else}} -
Contact us to make adminstrator changes.
+ {{/if}} diff --git a/src/api-umbrella/admin-ui/app/templates/api-users/index.hbs b/src/api-umbrella/admin-ui/app/templates/api-users/index.hbs index 719a44902..2df336753 100644 --- a/src/api-umbrella/admin-ui/app/templates/api-users/index.hbs +++ b/src/api-umbrella/admin-ui/app/templates/api-users/index.hbs @@ -1,7 +1,7 @@

API Users

{{#if this.currentAdmin.permissions.user_manage}} -
+
Add New API User
{{/if}} diff --git a/src/api-umbrella/admin-ui/app/templates/apis/index.hbs b/src/api-umbrella/admin-ui/app/templates/apis/index.hbs index 30f615146..c7c639058 100644 --- a/src/api-umbrella/admin-ui/app/templates/apis/index.hbs +++ b/src/api-umbrella/admin-ui/app/templates/apis/index.hbs @@ -1,6 +1,6 @@

API Backends

-
+
Add API Backend
diff --git a/src/api-umbrella/admin-ui/app/templates/application.hbs b/src/api-umbrella/admin-ui/app/templates/application.hbs index 12941ba8e..a193ad908 100644 --- a/src/api-umbrella/admin-ui/app/templates/application.hbs +++ b/src/api-umbrella/admin-ui/app/templates/application.hbs @@ -1,49 +1,49 @@ {{#if this.session.isAuthenticated}} -