@@ -14,13 +22,19 @@
@value={{datum}}
aria-label="Data Input"
placeholder={{@placeholder}}
+ disabled={{this.disabled}}
+ class="form-input w-full flex-1 border-none shadow-none rounded-none pr-24"
{{on "change" (fn this.onChange index)}}
{{on "paste" (fn this.onPaste index)}}
- class="form-input w-full flex-1 border-none shadow-none rounded-none pr-24"
{{on "keyup" (fn this.inputDatum index)}}
/>
-
+
{{/each}}
diff --git a/addon/components/array-input.js b/addon/components/array-input.js
index 78bf092..cc6ec8e 100644
--- a/addon/components/array-input.js
+++ b/addon/components/array-input.js
@@ -4,11 +4,13 @@ import { action } from '@ember/object';
export default class ArrayInputComponent extends Component {
@tracked data = [];
+ @tracked disabled = false;
- constructor() {
+ constructor(owner, { data = [], disabled = false }) {
super(...arguments);
- this.data = this.args.data ?? [];
+ this.data = data;
+ this.disabled = disabled;
}
@action onChange(index, event) {
diff --git a/addon/components/button.js b/addon/components/button.js
index 9e04667..d3f2abe 100644
--- a/addon/components/button.js
+++ b/addon/components/button.js
@@ -48,6 +48,13 @@ export default class ButtonComponent extends Component {
return icon && !isLoading;
}
+ /**
+ * The permission required.
+ *
+ * @memberof ButtonComponent
+ */
+ @tracked permissionRequired;
+
/**
* If the button is disabled by permissions.
*
diff --git a/addon/components/checkbox.hbs b/addon/components/checkbox.hbs
index d885386..92b180d 100644
--- a/addon/components/checkbox.hbs
+++ b/addon/components/checkbox.hbs
@@ -1,32 +1,40 @@
-
-
-
- {{#if @helpText}}
-
-
-
- {{/if}}
-
- {{#if (has-block)}}
-
-
-
- {{else if @label}}
-
-
+{{#if this.visible}}
+
+
+
+ {{#if (has-block)}}
+
+
+
+ {{else if @label}}
+
+
+
+ {{/if}}
+ {{#if this.disabledByPermission}}
+
+
+
+ {{else if @helpText}}
+
+
+
+ {{/if}}
- {{/if}}
-
\ No newline at end of file
+
+
+{{/if}}
\ No newline at end of file
diff --git a/addon/components/checkbox.js b/addon/components/checkbox.js
index a0f3b57..42de8b5 100644
--- a/addon/components/checkbox.js
+++ b/addon/components/checkbox.js
@@ -1,9 +1,12 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { computed, action } from '@ember/object';
+import { inject as service } from '@ember/service';
import { guidFor } from '@ember/object/internals';
export default class CheckboxComponent extends Component {
+ @service abilities;
+
/**
* Generates a unique ID for this checkbox instance
*
@@ -33,6 +36,44 @@ export default class CheckboxComponent extends Component {
*/
@tracked colorClass = 'text-sky-500';
+ /**
+ * The permission required.
+ *
+ * @memberof CheckboxComponent
+ */
+ @tracked permissionRequired;
+
+ /**
+ * If the button is disabled by permissions.
+ *
+ * @memberof CheckboxComponent
+ */
+ @tracked disabledByPermission = false;
+
+ /**
+ * Determines the visibility of the button
+ *
+ * @memberof CheckboxComponent
+ */
+ @tracked visible = true;
+
+ /**
+ * Creates an instance of ButtonComponent.
+ * @param {*} owner
+ * @param {*} { permission = null }
+ * @memberof ButtonComponent
+ */
+ constructor(owner, { value = false, permission = null, disabled = false, visible = true }) {
+ super(...arguments);
+ this.checked = value;
+ this.permissionRequired = permission;
+ this.visible = visible;
+ this.disabled = disabled;
+ if (!disabled) {
+ this.disabled = this.disabledByPermission = permission && this.abilities.cannot(permission);
+ }
+ }
+
/**
* Toggles the checkbox and sends up an action
*
diff --git a/addon/components/layout/header.hbs b/addon/components/layout/header.hbs
index 214e8d0..7c41ba5 100644
--- a/addon/components/layout/header.hbs
+++ b/addon/components/layout/header.hbs
@@ -8,7 +8,7 @@
{{/if}}
{{#unless (media "isMobile")}}
-
+
diff --git a/addon/components/layout/header.js b/addon/components/layout/header.js
index 852c14a..4e0dbaf 100644
--- a/addon/components/layout/header.js
+++ b/addon/components/layout/header.js
@@ -1,8 +1,9 @@
import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
-import { computed, action } from '@ember/object';
-import { alias } from '@ember/object/computed';
-import { isArray } from '@ember/array';
+import { action } from '@ember/object';
+import { getOwner } from '@ember/application';
+import config from 'ember-get-config';
/**
* Layout header component.
@@ -16,78 +17,136 @@ export default class LayoutHeaderComponent extends Component {
@service router;
@service hostRouter;
@service universe;
+ @service currentUser;
+ @service abilities;
+ @service fetch;
+ @tracked company;
+ @tracked menuItems = [];
+ @tracked organizationMenuItems = [];
+ @tracked userMenuItems = [];
+ @tracked extensions = [];
- @alias('args.user') user;
-
- @computed('store', 'user.company_uuid') get company() {
- return this.store.peekRecord('company', this.user.company_uuid);
+ constructor(owner, { menuItems = [], organizationMenuItems = [], userMenuItems = [] }) {
+ super(...arguments);
+ this.extensions = getOwner(this).application.extensions ?? [];
+ this.company = this.currentUser.getCompany();
+ this.menuItems = this.mergeMenuItems(menuItems);
+ this.organizationMenuItems = this.mergeOrganizationMenuItems(organizationMenuItems);
+ this.userMenuItems = this.mergeUserMenuItems(userMenuItems);
}
- @computed('args.{organizationNavigationItems,organizations.length}', 'organizations.@each.id', 'universe.organizationMenuItems', 'user.{company_name,email}')
- get organizationNavigationItems() {
- const universeOrganizationItems = this.universe.organizationMenuItems;
+ mergeMenuItems(menuItems = []) {
+ const headerMenuItems = this.universe.headerMenuItems;
+ const visibleMenuItems = [];
+ for (let i = 0; i < headerMenuItems.length; i++) {
+ const menuItem = headerMenuItems[i];
+ if (this.abilities.can(`${menuItem.slug} see extension`)) {
+ visibleMenuItems.pushObject(menuItem);
+ }
+ }
- if (isArray(this.args.organizationNavigationItems)) {
- const items = this.args.organizationNavigationItems;
+ // Merge additionals
+ visibleMenuItems.pushObjects(menuItems);
- if (universeOrganizationItems) {
- for (let i = 0; i < universeOrganizationItems.length; i++) {
- const menuItem = universeOrganizationItems[i];
- menuItem.text = menuItem.title;
- items.insertAt(menuItem.index, menuItem);
- }
- }
+ // Callback to allow mutation of menu items
+ if (typeof this.args.mutateMenuItems === 'function') {
+ this.args.mutateMenuItems(menuItems);
}
- const items = [
+ return visibleMenuItems;
+ }
+
+ mergeOrganizationMenuItems(organizationMenuItems = []) {
+ // Prepare menuItems
+ const menuItems = [
{
- text: [this.user.email, this.user.company_name],
+ text: [this.currentUser.email, this.currentUser.companyName],
class: 'flex flex-row items-center px-3 rounded-md text-gray-800 text-sm dark:text-gray-300 leading-1',
wrapperClass: 'next-dd-session-user-wrapper',
},
- {
- seperator: true,
- },
];
- this.args.organizations?.forEach((organization) => {
- items.pushObject({
+ // List available organizations for session switching
+ const organizations = this.currentUser.organizations;
+ if (organizations.length) {
+ menuItems.pushObject({ seperator: true });
+ }
+ for (let i = 0; i < organizations.length; i++) {
+ const organization = organizations.objectAt(i);
+ const organizationMenuItem = {
href: 'javascript:;',
text: organization.name,
action: 'switchOrganization',
params: [organization],
- });
- });
+ };
+
+ // If current organization
+ if (this.currentUser.companyId === organization.id) {
+ organizationMenuItem.icon = 'check';
+ organizationMenuItem.disabled = true;
+ organizationMenuItem.action = undefined;
+ }
+
+ menuItems.pushObject(organizationMenuItem);
+ }
- items.pushObjects([
+ // Push static menu items
+ const staticMenuItems = [
{
seperator: true,
},
{
+ id: 'console-home',
route: 'console.home',
text: 'Home',
icon: 'house',
},
{
+ id: 'organization-settings',
route: 'console.settings.index',
text: 'Organization settings',
icon: 'gear',
},
{
+ id: 'create-or-join-organizations',
href: 'javascript:;',
text: 'Create or join organizations',
action: 'createOrJoinOrg',
icon: 'building',
},
- {
+ ];
+
+ // If registry bridge is booted add to static items
+ if (this.hasExtension('@fleetbase/registry-bridge')) {
+ staticMenuItems.pushObject({
+ id: 'explore-extensions',
route: 'console.extensions',
text: 'Explore extensions',
icon: 'puzzle-piece',
- },
- ]);
+ });
+ }
+
+ // Push static items
+ menuItems.pushObjects(staticMenuItems);
+
+ // Merge provided menu items
+ menuItems.pushObjects(organizationMenuItems);
+
+ // Push the version
+ menuItems.pushObject({
+ id: 'app-version',
+ route: null,
+ text: `v${config.version}`,
+ icon: 'code-branch',
+ iconSize: 'xs',
+ iconClass: 'mr-1.5',
+ wrapperClass: 'app-version-in-nav',
+ overwriteWrapperClass: true,
+ });
- if (this.user.get('is_admin')) {
- items.pushObjects([
+ // Merge admin link
+ if (this.currentUser.isAdmin) {
+ menuItems.pushObjects([
{
seperator: true,
},
@@ -99,7 +158,8 @@ export default class LayoutHeaderComponent extends Component {
]);
}
- items.pushObjects([
+ // Merge logout link
+ menuItems.pushObjects([
{
seperator: true,
},
@@ -111,41 +171,37 @@ export default class LayoutHeaderComponent extends Component {
},
]);
+ // Get organization menu items from registry
+ const universeOrganizationItems = this.universe.organizationMenuItems;
if (universeOrganizationItems) {
- const preIndex = (this.args.organizations?.length ?? 0) + 3;
+ const preIndex = (organizations.length ?? 0) + (staticMenuItems.length ?? 0);
for (let i = 0; i < universeOrganizationItems.length; i++) {
const menuItem = universeOrganizationItems[i];
menuItem.text = menuItem.title;
- items.insertAt(preIndex + menuItem.index, menuItem);
+ menuItems.insertAt(preIndex + menuItem.index, menuItem);
}
}
- return items;
- }
-
- @computed('args.{userNavigationItems,extensions}', 'universe.userMenuItems') get userNavigationItems() {
- const universeUserMenuItems = this.universe.userMenuItems;
-
- if (isArray(this.args.userNavigationItems)) {
- const items = this.args.userNavigationItems;
-
- if (universeUserMenuItems) {
- for (let i = 0; i < universeUserMenuItems.length; i++) {
- const menuItem = universeUserMenuItems[i];
- menuItem.text = menuItem.title;
- items.insertAt(menuItem.index, menuItem);
- }
- }
-
- return items;
+ // Callback to allow mutation of menu items
+ if (typeof this.args.mutateOrganizationMenuItems === 'function') {
+ this.args.mutateOrganizationMenuItems(menuItems);
}
- const items = [
+ return menuItems;
+ }
+
+ mergeUserMenuItems(userMenuItems = []) {
+ // Prepare menu items
+ const menuItems = [
{
+ id: 'view-profile-user-nav-item',
+ wrapperClass: 'view-profile-user-nav-item',
route: 'console.account.index',
text: 'View Profile',
},
{
+ id: 'show-keyboard-shortcuts-user-nav-item',
+ wrapperClass: 'show-keyboard-shortcuts-user-nav-item',
href: 'javascript:;',
text: 'Show keyboard shortcuts',
disabled: true,
@@ -155,38 +211,59 @@ export default class LayoutHeaderComponent extends Component {
seperator: true,
},
{
+ id: 'changelog-user-nav-item',
+ wrapperClass: 'changelog-user-nav-item',
href: 'javascript:;',
text: 'Changelog',
action: 'viewChangelog',
},
];
+ // Add developer menu item if booted
if (this.hasExtension('@fleetbase/dev-engine')) {
- items.pushObject({
+ menuItems.pushObject({
+ id: 'developers-user-nav-item',
+ wrapperClass: 'developers-user-nav-item',
route: 'console.developers',
text: 'Developers',
});
}
- items.pushObjects([
+ // Add more static menu items
+ const supportMenuItems = [
{
+ id: 'discord',
href: 'https://discord.gg/MJQgxHwN',
target: '_discord',
text: 'Join Discord Community',
icon: 'arrow-up-right-from-square',
},
{
+ id: 'support-user-nav-item',
+ wrapperClass: 'support-user-nav-item',
href: 'https://github.com/fleetbase/fleetbase/issues',
target: '_support',
text: 'Help & Support',
icon: 'arrow-up-right-from-square',
},
{
- href: 'https://fleetbase.github.io/api-reference/',
- target: '_api',
- text: 'API Reference',
+ id: 'docs-user-nav-item',
+ wrapperClass: 'docs-user-nav-item',
+ href: 'https://docs.fleetbase.io',
+ target: '_docs',
+ text: 'Documentation',
icon: 'arrow-up-right-from-square',
},
+ ];
+
+ // Push support menu items
+ menuItems.pushObjects(supportMenuItems);
+
+ // Push provided menu items
+ menuItems.pushObjects(userMenuItems);
+
+ // Create immutable static menu items
+ menuItems.pushObjects([
{
component: 'layout/header/dark-mode-toggle',
},
@@ -200,15 +277,12 @@ export default class LayoutHeaderComponent extends Component {
},
]);
- if (universeUserMenuItems) {
- for (let i = 0; i < universeUserMenuItems.length; i++) {
- const menuItem = universeUserMenuItems[i];
- menuItem.text = menuItem.title;
- items.insertAt(menuItem.index, menuItem);
- }
+ // Callback to allow mutation of menu items
+ if (typeof this.args.mutateUserMenuItems === 'function') {
+ this.args.mutateUserMenuItems(menuItems);
}
- return items;
+ return menuItems;
}
@action routeTo(route) {
@@ -218,7 +292,6 @@ export default class LayoutHeaderComponent extends Component {
}
hasExtension(extensionName) {
- const { extensions } = this.args;
- return extensions.find(({ name }) => name === extensionName);
+ return this.extensions.find(({ name }) => name === extensionName);
}
}
diff --git a/addon/components/modals/changelog.hbs b/addon/components/modals/changelog.hbs
index 7c59443..d7b23c8 100644
--- a/addon/components/modals/changelog.hbs
+++ b/addon/components/modals/changelog.hbs
@@ -7,14 +7,17 @@
{{else}}
{{#each this.releases as |release|}}
-
-
{{release.name}}
+
+
{{release.name}}
+ •
{{release.created_at}}
{{#each release.changes as |change|}}
- - {{change}}
+ {{#if change}}
+ - {{change}}
+ {{/if}}
{{/each}}
diff --git a/addon/components/toggle.hbs b/addon/components/toggle.hbs
index 0953374..40ec90d 100644
--- a/addon/components/toggle.hbs
+++ b/addon/components/toggle.hbs
@@ -1,31 +1,41 @@
-
-
-
+{{#if this.visible}}
+
-
-
- {{yield}}
- {{#if @label}}
-
{{@label}}
- {{/if}}
- {{#if @helpText}}
-
-
-
-
+ role="checkbox"
+ tabindex="0"
+ aria-checked="false"
+ class="relative inline-flex items-center justify-center flex-shrink-0 w-10 h-5 cursor-pointer group focus:outline-none {{if this.disabled 'opacity-50'}}"
+ data-disabled={{this.disabled}}
+ ...attributes
+ {{on "click" (fn this.toggle this.isToggled)}}
+ >
+
+
+
+
+ {{yield}}
+ {{#if @label}}
+
{{@label}}
+ {{/if}}
+ {{#if this.disabledByPermission}}
+
+
-
- {{/if}}
+ {{else if @helpText}}
+
+ {{/if}}
+
-
\ No newline at end of file
+{{/if}}
\ No newline at end of file
diff --git a/addon/components/toggle.js b/addon/components/toggle.js
index 536603c..6f3b164 100644
--- a/addon/components/toggle.js
+++ b/addon/components/toggle.js
@@ -1,8 +1,11 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
import { action, computed } from '@ember/object';
export default class ToggleComponent extends Component {
+ @service abilities;
+
/**
* The active color of the toggle
*
@@ -17,6 +20,27 @@ export default class ToggleComponent extends Component {
*/
@tracked activeColor = 'green';
+ /**
+ * The permission required.
+ *
+ * @memberof ToggleComponent
+ */
+ @tracked permissionRequired;
+
+ /**
+ * If the button is disabled by permissions.
+ *
+ * @memberof ToggleComponent
+ */
+ @tracked disabledByPermission = false;
+
+ /**
+ * Determines the visibility of the button
+ *
+ * @memberof ToggleComponent
+ */
+ @tracked visible = true;
+
/**
* The active color class.
* Defaults to `bg-green-400` but could also be:
@@ -33,11 +57,18 @@ export default class ToggleComponent extends Component {
*
* @memberof ToggleComponent
*/
- constructor(owner, { isToggled, activeColor }) {
+ constructor(owner, { value = false, isToggled = false, activeColor = 'green', permission = null, disabled = false, visible = true }) {
super(...arguments);
- this.isToggled = isToggled === true;
- this.activeColor = typeof activeColor === 'string' ? activeColor : 'green';
+ this.isToggled = isToggled;
+ this.activeColor = activeColor;
+ this.checked = value;
+ this.permissionRequired = permission;
+ this.visible = visible;
+ this.disabled = disabled;
+ if (!disabled) {
+ this.disabled = this.disabledByPermission = permission && this.abilities.cannot(permission);
+ }
}
/**
@@ -46,15 +77,14 @@ export default class ToggleComponent extends Component {
* @void
*/
@action toggle(isToggled) {
- const { disabled, onToggle } = this.args;
- if (disabled) {
+ if (this.disabled) {
return;
}
this.isToggled = !isToggled;
- if (typeof onToggle === 'function') {
- onToggle(this.isToggled);
+ if (typeof this.args.onToggle === 'function') {
+ this.args.onToggle(this.isToggled);
}
}
diff --git a/addon/helpers/component-resolvable.js b/addon/helpers/component-resolvable.js
new file mode 100644
index 0000000..cc9bc73
--- /dev/null
+++ b/addon/helpers/component-resolvable.js
@@ -0,0 +1,10 @@
+import Helper from '@ember/component/helper';
+import { getOwner } from '@ember/application';
+
+export default class ComponentResolvableHelper extends Helper {
+ compute(params) {
+ const [componentName] = params;
+ const owner = getOwner(this);
+ return owner && owner.hasRegistration(`component:${componentName}`);
+ }
+}
diff --git a/addon/helpers/has-registration.js b/addon/helpers/has-registration.js
new file mode 100644
index 0000000..3fff3d3
--- /dev/null
+++ b/addon/helpers/has-registration.js
@@ -0,0 +1,21 @@
+import Helper from '@ember/component/helper';
+import { getOwner } from '@ember/application';
+
+/**
+ * Checks if a registration exists in the ApplicationInstance container registry.
+ * Usage: `(has-registration 'component:my-component')` or `(has-registration 'my-component' 'component')`
+ *
+ * @export
+ * @class HasRegistrationHelper
+ * @extends {Helper}
+ */
+export default class HasRegistrationHelper extends Helper {
+ compute(params) {
+ let [name, type] = params;
+ if (type) {
+ name = `${type}:${name}`;
+ }
+ const owner = getOwner(this);
+ return owner && owner.hasRegistration(name);
+ }
+}
diff --git a/app/helpers/component-resolvable.js b/app/helpers/component-resolvable.js
new file mode 100644
index 0000000..46bd5d7
--- /dev/null
+++ b/app/helpers/component-resolvable.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/ember-ui/helpers/component-resolvable';
diff --git a/app/helpers/has-registration.js b/app/helpers/has-registration.js
new file mode 100644
index 0000000..2283e59
--- /dev/null
+++ b/app/helpers/has-registration.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/ember-ui/helpers/has-registration';
diff --git a/tests/integration/helpers/component-resolvable-test.js b/tests/integration/helpers/component-resolvable-test.js
new file mode 100644
index 0000000..ba3d59a
--- /dev/null
+++ b/tests/integration/helpers/component-resolvable-test.js
@@ -0,0 +1,17 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Helper | component-resolvable', function (hooks) {
+ setupRenderingTest(hooks);
+
+ // TODO: Replace this with your real tests.
+ test('it renders', async function (assert) {
+ this.set('inputValue', '1234');
+
+ await render(hbs`{{component-resolvable this.inputValue}}`);
+
+ assert.dom().hasText('1234');
+ });
+});
diff --git a/tests/integration/helpers/has-registration-test.js b/tests/integration/helpers/has-registration-test.js
new file mode 100644
index 0000000..bf6a888
--- /dev/null
+++ b/tests/integration/helpers/has-registration-test.js
@@ -0,0 +1,17 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Helper | has-registration', function (hooks) {
+ setupRenderingTest(hooks);
+
+ // TODO: Replace this with your real tests.
+ test('it renders', async function (assert) {
+ this.set('inputValue', '1234');
+
+ await render(hbs`{{has-registration this.inputValue}}`);
+
+ assert.dom().hasText('1234');
+ });
+});