Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Broker Topic Hierarchy UI #4790

Merged
merged 29 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8c0ccb1
add uns scaffolding
cstns Nov 12, 2024
83b0027
Merge remote-tracking branch 'origin/main' into unified-name-space
cstns Nov 13, 2024
1159131
address useStore utility reactivity in composables
cstns Nov 13, 2024
417bced
Merge remote-tracking branch 'origin/track-used-topics' into unified-…
cstns Nov 13, 2024
52acf29
Merge remote-tracking branch 'origin/track-used-topics' into unified-…
cstns Nov 15, 2024
3c0e7bc
add page icons
cstns Nov 15, 2024
540a7c2
add topic api client
cstns Nov 15, 2024
811e79c
add hierarchy page scaffolding, get topics on page load and bare-bone…
cstns Nov 15, 2024
276817f
add hierarchy styling
cstns Nov 15, 2024
19eb90d
styling
cstns Nov 18, 2024
959e278
Merge branch 'track-used-topics' into unified-name-space
cstns Nov 18, 2024
77826bf
fix existing e2e tests
cstns Nov 18, 2024
55b3176
fix missed nav renaming, align the hierarchy page redirects based on …
cstns Nov 18, 2024
75d48a3
qf test names
cstns Nov 18, 2024
3f4cd74
qf styling
cstns Nov 18, 2024
9745c9e
add e2e tests
cstns Nov 18, 2024
b3d1f47
Update frontend/src/pages/team/UNS/index.vue
cstns Nov 18, 2024
4f9c59b
qf e2e tests after changing the empty state message
cstns Nov 18, 2024
a7aa0bd
revert menu entry and url
cstns Nov 19, 2024
f4049dc
Merge remote-tracking branch 'origin/main' into unified-name-space
cstns Nov 19, 2024
2f87828
merge fixes
cstns Nov 19, 2024
9a421ea
Merge branch 'main' into unified-name-space
cstns Nov 20, 2024
a83dcf8
update uns context title
cstns Nov 20, 2024
9473425
Update frontend/src/pages/team/routes.js
cstns Nov 20, 2024
d70f1fe
Update frontend/src/pages/team/UNS/index.vue
cstns Nov 20, 2024
7a9111a
Update frontend/src/pages/team/UNS/index.vue
cstns Nov 20, 2024
3d3c993
Merge remote-tracking branch 'origin/unified-name-space' into unified…
cstns Nov 20, 2024
a8170e0
qf failing tests
cstns Nov 20, 2024
c27a4f5
Merge branch 'main' into unified-name-space
cstns Nov 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion frontend/src/api/broker.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,16 @@ const updateClient = (teamId, username, { acls, password }) => {
}).then(res => res.data)
}

const getTopics = (teamId) => {
return client.get(`/api/v1/teams/${teamId}/broker/topics`)
.then(res => res.data)
}

export default {
getClients,
getClient,
createClient,
updateClient,
deleteClient
deleteClient,
getTopics
}
84 changes: 45 additions & 39 deletions frontend/src/composables/Permissions.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,44 @@
// import { useStore } from 'vuex'
import { useStore } from 'vuex'

import { Permissions } from '../../../forge/lib/permissions.js'
import { Roles } from '../utils/roles.js'

/**
* Fixes reactivity issue with the useStore utility but throws a warning while in dev mode
* [Vue warn]: inject() can only be used inside setup() or functional components
* Since teamMembership is defined outside the composable function and updated conditionally in usePermissions()
*
* The initialized store returns undefined upon switching teams and looses reactivity afterward. This should
* warrant more investigations, as this usually happens when the entire store is re-written.
*
* @type {{role: number}}
*/
let teamMembership = { role: 0 }

/**
* @typedef {0 | 5 | 10 | 30 | 50 | 99} Role
* Enum for roles with specific numeric values.
*/

export default function usePermissions () {
// todo There's a reactivity problem I can't figure out with the useStore utility and our account store when switching
// teams. The initialized store returns undefined upon switching teams and looses reactivity afterwards. This should
// warrant more investigations, as this usually happens when the entire store is re-written. The teamMembership arguments
// should be disposed asap
const store = useStore()

// const store = useStore()
// const teamMembership = store.state.account.teamMembership
if (store && store?.state?.account?.teamMembership) {
teamMembership = store?.state?.account?.teamMembership
}

/**
* @param teamMembership
* @returns {boolean}
*/
const isVisitingAdmin = (teamMembership) => {
return teamMembership === Roles.Admin
* @returns {boolean}
*/
const isVisitingAdmin = () => {
return teamMembership.role === Roles.Admin
}

/**
*
* @param scope
* @param teamMembership
* @returns {boolean}
*/
const hasPermission = (scope, teamMembership) => {
*
* @param scope
* @returns {boolean}
*/
const hasPermission = (scope) => {
if (!Permissions[scope]) {
throw new Error(`Unrecognised scope requested: '${scope}'`)
}
Expand All @@ -48,33 +56,31 @@ export default function usePermissions () {
}

/**
* Check if the user has the minimum required role.
* @param {Role} role - The role to check against.
* @param teamMembership
* @returns {boolean} True if the user has the minimum required role, otherwise false.
* @example
* // Check if the user has at least the 'Member' role
* const isMemberOrHigher = hasAMinimumTeamRoleOf(Roles.Member)
*/
const hasAMinimumTeamRoleOf = (role, teamMembership) => {
if (isVisitingAdmin(teamMembership?.role)) {
* Check if the user has the minimum required role.
* @param {Role} role - The role to check against.
* @returns {boolean} True if the user has the minimum required role, otherwise false.
* @example
* // Check if the user has at least the 'Member' role
* const isMemberOrHigher = hasAMinimumTeamRoleOf(Roles.Member)
*/
const hasAMinimumTeamRoleOf = (role) => {
if (isVisitingAdmin()) {
return true
}

return teamMembership?.role >= role
}

/**
* Check if the user has a lower role than given role.
* @param {Role} role - The role to check against.
* @param teamMembership
* @returns {boolean} True if the user has a lower role than the given one, otherwise false.
* @example
* // Check if the user has role lower than 'Member' role
* const isMemberOrHigher = hasALowerTeamRoleThan(Roles.Member)
*/
const hasALowerOrEqualTeamRoleThan = (role, teamMembership) => {
if (isVisitingAdmin(teamMembership?.role)) {
* Check if the user has a lower role than given role.
* @param {Role} role - The role to check against.
* @returns {boolean} True if the user has a lower role than the given one, otherwise false.
* @example
* // Check if the user has role lower than 'Member' role
* const isMemberOrHigher = hasALowerTeamRoleThan(Roles.Member)
*/
const hasALowerOrEqualTeamRoleThan = (role) => {
if (isVisitingAdmin()) {
return true
}

Expand Down
16 changes: 16 additions & 0 deletions frontend/src/images/icons/tree-view.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 6 additions & 11 deletions frontend/src/mixins/Permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,20 @@ import usePermissions from '../composables/Permissions.js'
* @typedef {0 | 5 | 10 | 30 | 50 | 99} Role
* Enum for roles with specific numeric values.
*/

/**
* @typedef {0 | 5 | 10 | 30 | 50 | 99} Role
* Enum for roles with specific numeric values.
*/

// todo in an attempt to sunset the wide use of mixins, the permissions composable should be used instead
export default {
computed: {
// todo to be removed. A lot of components that use this mixin rely on the state imported here
...mapState('account', ['team', 'teamMembership']),

isVisitingAdmin () {
const { isVisitingAdmin } = usePermissions()
return isVisitingAdmin(this.teamMembership?.role)
return isVisitingAdmin()
}
},
methods: {
hasPermission (scope) {
const { hasPermission } = usePermissions()
return hasPermission(scope, this.teamMembership)
return hasPermission(scope)
},

/**
Expand All @@ -37,7 +32,7 @@ export default {
*/
hasAMinimumTeamRoleOf (role) {
const { hasAMinimumTeamRoleOf } = usePermissions()
return hasAMinimumTeamRoleOf(role, this.teamMembership)
return hasAMinimumTeamRoleOf(role)
},

/**
Expand All @@ -50,7 +45,7 @@ export default {
*/
hasALowerOrEqualTeamRoleThan (role) {
const { hasALowerOrEqualTeamRoleThan } = usePermissions()
return hasALowerOrEqualTeamRoleThan(role, this.teamMembership)
return hasALowerOrEqualTeamRoleThan(role)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@

<script>
import { PencilIcon, TrashIcon } from '@heroicons/vue/outline'
import { mapState } from 'vuex'

import FfAccordion from '../../../../components/Accordion.vue'
import TextCopier from '../../../../components/TextCopier.vue'
import permissionsMixin from '../../../../mixins/Permissions.js'
import { Roles } from '../../../../utils/roles.js'
import FfAccordion from '../../../../../components/Accordion.vue'
import TextCopier from '../../../../../components/TextCopier.vue'
import usePermissions from '../../../../../composables/Permissions.js'
import { Roles } from '../../../../../utils/roles.js'

import BrokerAclRule from './BrokerAclRule.vue'

Expand All @@ -64,15 +65,22 @@ export default {
FfAccordion,
TrashIcon
},
mixins: [permissionsMixin],
props: {
client: {
required: true,
type: Object
}
},
emits: ['edit-client', 'delete-client'],
setup () {
const { hasAMinimumTeamRoleOf } = usePermissions()

return {
hasAMinimumTeamRoleOf
}
},
computed: {
...mapState('account', ['team']),
Roles () {
return Roles
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
<script>
import { MinusIcon } from '@heroicons/vue/solid'

import FormRow from '../../../../components/FormRow.vue'
import FfListbox from '../../../../ui-components/components/form/ListBox.vue'
import FormRow from '../../../../../components/FormRow.vue'
import FfListbox from '../../../../../ui-components/components/form/ListBox.vue'

export default {
name: 'AclItem',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@
import { PlusIcon } from '@heroicons/vue/solid'
import { mapState } from 'vuex'

import brokerApi from '../../../../api/broker.js'
import FormRow from '../../../../components/FormRow.vue'
import { generateUuid } from '../../../../composables/String.js'
import brokerApi from '../../../../../api/broker.js'
import FormRow from '../../../../../components/FormRow.vue'
import { generateUuid } from '../../../../../composables/String.js'

import AclItem from './AclItem.vue'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
<template>
<ff-page>
<template #header>
<ff-page-header title="MQTT Broker">
<template #context>
Central hub for managing MQTT clients and defining ACL-based topic permissions.
</template>
<template #pictogram>
<img alt="info" src="../../../images/pictograms/mqtt_broker_red.png">
</template>
<template #helptext>
<p>The <b>MQTT Broker</b> page offers a streamlined interface for managing your broker instance and defining client connections.</p>
<p>You can create and manage MQTT clients, each with customizable Access Control List (ACL) rules to ensure secure and controlled communication. The ACL rules allow for fine-grained control over each client’s access to specific topics, supporting both publishing and subscribing actions.</p>
<p>This overview provides a clear and organized view of your broker’s configuration, helping you manage client connections, security settings, and message flow efficiently.</p>
<p>Documentation <a href="https://flowfuse.com/docs/user/teambroker" target="_blank">here</a></p>
</template>
</ff-page-header>
</template>
<div class="unified-namespace-clients">
<div class="title mb-5 flex gap-3 items-center">
<RssIcon class="ff-icon-sm" />
<h3 class="m-0" data-el="subtitle">MQTT Broker</h3>
</div>

<EmptyState
v-if="!isMqttBrokerFeatureEnabled"
:featureUnavailable="!isMqttBrokerFeatureEnabledForPlatform"
:featureUnavailableToTeam="!isMqttBrokerFeatureEnabledForTeam"
>
<template #img>
<img src="../../../images/empty-states/mqtt-forbidden.png" alt="pipelines-logo">
<img src="../../../../images/empty-states/mqtt-forbidden.png" alt="pipelines-logo">
</template>
<template #header>
<span>Broker Not Available</span>
Expand Down Expand Up @@ -82,7 +70,7 @@
</section>
<EmptyState v-else>
<template #img>
<img src="../../../images/empty-states/mqtt-empty.png" alt="logo">
<img src="../../../../images/empty-states/mqtt-empty.png" alt="logo">
</template>
<template #header>Create your first Broker Client</template>
<template #message>
Expand Down Expand Up @@ -112,32 +100,33 @@
@client-updated="fetchData"
/>
</template>
</ff-page>
</div>
</template>

<script>
import { PlusSmIcon, SearchIcon } from '@heroicons/vue/outline'
import { PlusSmIcon, RssIcon, SearchIcon } from '@heroicons/vue/outline'
import { mapState } from 'vuex'

import brokerApi from '../../../api/broker.js'
import EmptyState from '../../../components/EmptyState.vue'
import clipboardMixin from '../../../mixins/Clipboard.js'
import featuresMixin from '../../../mixins/Features.js'
import permissionsMixin from '../../../mixins/Permissions.js'
import Alerts from '../../../services/alerts.js'
import Dialog from '../../../services/dialog.js'
import { Roles } from '../../../utils/roles.js'
import brokerApi from '../../../../api/broker.js'
import EmptyState from '../../../../components/EmptyState.vue'
import clipboardMixin from '../../../../mixins/Clipboard.js'
import featuresMixin from '../../../../mixins/Features.js'
import permissionsMixin from '../../../../mixins/Permissions.js'
import Alerts from '../../../../services/alerts.js'
import Dialog from '../../../../services/dialog.js'
import { Roles } from '../../../../utils/roles.js'

import BrokerClient from './components/BrokerClient.vue'

import ClientDialog from './dialogs/ClientDialog.vue'

export default {
name: 'TeamBroker',
name: 'UNSClients',
components: {
BrokerClient,
SearchIcon,
PlusSmIcon,
RssIcon,
EmptyState,
ClientDialog
},
Expand Down Expand Up @@ -168,9 +157,6 @@ export default {
team: 'fetchData'
},
mounted () {
if (!this.hasAMinimumTeamRoleOf(Roles.Member)) {
return this.$router.push({ name: 'Home' })
}
this.fetchData()
},
methods: {
Expand Down
Loading
Loading