Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
4 changes: 2 additions & 2 deletions cypress/integration/ambianic-tests/navbar.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ context('Check Navbar Items', () => {
cy.visit('http://localhost:8080/about')
})

it('Should be a download off button before edge connection', () => {
cy.get('[data-cy=download-off]').should('exist')
it('Should display a connection-offline icon before edge connection', () => {
cy.get('[data-cy=connection-status]').should('exist')
})

/** future buttons
Expand Down
1 change: 1 addition & 0 deletions cypress/integration/ambianic-tests/timeline.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ context('Timeline', () => {
cy.url().should('include', '/timeline')
})


// Try to get this to work once we have mock data

// it('Get timeline data', () => {
Expand Down
4 changes: 3 additions & 1 deletion src/components/AppFrame.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<div>
<!-- nav goes here -->
<NavBar />
<ComponentStatusSnack />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vickywane usually Vue component names are not prefixed with Component. It becomes redundant and noisy as components add up. Let's rename to statusSnackbar to keep it close to the standard meaning of the material design component names.


<v-main>
<v-container
Expand All @@ -27,11 +28,12 @@

<script>
import NavBar from './NavBar.vue'
import ComponentStatusSnack from '@/components/shared/snackNotification'

export default {
name: 'AppFrame',
components: {
UpdateNotification: () => import('./UpdateNotification'), NavBar
UpdateNotification: () => import('./UpdateNotification'), NavBar, ComponentStatusSnack
},
data: () => ({
dialog: false,
Expand Down
60 changes: 49 additions & 11 deletions src/components/NavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,30 @@
to="timeline"
/>

<nav-button
data-cy="download-off"
icon="download-off"
color="warning"
v-if="!isEdgeConnected"
to="edge-connect"
/>
<div>
<v-tooltip bottom>
<template
v-if="!isEdgeConnected"
#activator="{ on, attrs }"
>
<div
v-bind="attrs"
v-on="on"
>
<nav-button
:id="connectionStatusIcon"
data-cy="connection-status"
:icon="connectionStatusIcon"
:color="connectionIconColor"
to="edge-connect"
v-bind="attrs"
v-on="on"
/>
</div>
</template>
<span>{{ connectionStatusTooltipText }}</span>
</v-tooltip>
</div>

<!-- Future navbar icons
<v-text-field
Expand Down Expand Up @@ -174,11 +191,14 @@ export default {
NavButton: () => import('./shared/Button.vue')
},
data: () => ({
connectionStatusIcon: 'cloud-off-outline',
dialog: false,
drawer: null, // hide drawer on mobile and show on desktop
on: true,
connectionStatusTooltipText: 'Disconnected',
newFavorites: 0,
newAlerts: 2,
connectionIconColor: 'warning',
logo: '../assets/logo5.svg',
items: [
{ icon: 'history', text: 'Timeline', link: '/timeline' },
Expand Down Expand Up @@ -215,6 +235,19 @@ export default {
{ icon: 'info', text: 'About Ambianic', link: '/about' }
]
}),
methods: {
setConnectionTooltipText () {
if (this.peerConnectionStatus === 'PEER_DISCONNECTED') {
this.connectionStatusTooltipText = 'Disconnected'
this.connectionStatusIcon = 'cloud-off-outline'
this.connectionIconColor = 'warning'
} else if (this.peerConnectionStatus === 'PEER_CONNECTING') {
this.connectionStatusIcon = 'cloud-sync-outline'
this.connectionIconColor = 'black'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vickywane let's use default colors such as warning and info to avoid problems when the OS switches between light and dark themes. We had this issue in the past several times when some contributors pick colors they like for their widgets which do not agree with the colors of widgets elsewhere in the app.

this.connectionStatusTooltipText = 'Connecting ...'
}
}
},
computed: {
...mapState({
isEdgeConnected: function (state) {
Expand All @@ -223,16 +256,21 @@ export default {
)
const isConnected = state.pnp.peerConnectionStatus === PEER_CONNECTED
return isConnected
}
},
peerConnectionStatus: state => state.pnp.peerConnectionStatus
})
},
created () {
if (!this.isEdgeConnected) {
this.$store.dispatch(PEER_DISCOVER)
}

this.setConnectionTooltipText()
},
watch: {
peerConnectionStatus: function () {
this.setConnectionTooltipText()
}
}
}
</script>

<style lang="scss" scoped>
</style>
82 changes: 82 additions & 0 deletions src/components/shared/snackNotification.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<template>
<div id="ConnectionStatusSnack-ctn">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vickywane this component shows its current snackbar message each time a new page is loaded. Try going from Timeline to Settings to About and see what happens. Once a message is shown, some flag should clear and the same message should not be shown again. We probably need a vuex mutation for the flag to take care of race conditions as users switch pages while new system messages are pushed.

We can also think about a message queue. If multiple system messages are pushed, queue them up and show them one at a time until the queue is empty. This could be another PR as it adds a fair bit of new logic to think through.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Micro-Learning Topic: Race condition (Detected by phrase)

What is this? (2min video)

A race condition is a flaw that produces an unexpected result when the timing of actions impact other actions.

Try this challenge in Secure Code Warrior

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid concern and also an advanced use case.

For a quick fix, I think we can limit the Snackbar to only the timeline screen while this edge case is being worked upon in a new PR.

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't consider this an edge case. A system wide message should be only shown once.
With your suggestion if the user goes away and back to the timeline screen, they will still see the last message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay.

Should I proceed with a fix for that in this PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please implement the flag to clear shown messages in this PR. Open a separate issue for system message queuing.

<v-snackbar v-model="visibility">
<p id="snack-message">
{{ message }}
</p>

<template #action="{ attrs }">
<div
v-bind="attrs"
id="close-icon"
@click="handleClose"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vickywane what about the case when the snackbar closes itself due to timeout and not a click?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you are trying to point out.

The x icon for closing the snackbar manually, and it can also close after a timeout

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point is that visibility is only switched to false on click, but not if the snackbar closes due to timeout. In effect the problem with showing the latest old message when switching between menu items sill remains.

>
<Button
icon="close"
color="white"
/>
</div>
</template>
</v-snackbar>
</div>
</template>

<script>
import { mapState } from 'vuex'
import AmbButton from '@/components/shared/Button'

export default {
name: 'ConnectionStatusSnack',
data: () => ({
visibility: true,
message: 'Connecting to Ambianic Edge device'
}),
components: {
Button: AmbButton
},
created () {
this.setConnectionStatusNotification()
// this.handleClose()
},
computed: {
...mapState({
peerConnectionStatus: state => state.pnp.peerConnectionStatus
})
},
methods: {
handleClose () {
this.visibility = false
},
setConnectionStatusNotification () {
switch (this.peerConnectionStatus) {
case 'PEER_CONNECTING':
this.message = 'Connecting to Ambianic Edge device'
this.visibility = true
break
case 'PEER_CONNECTED':
this.message = 'Connected to Ambianic Edge device'
this.visibility = true
break
case 'PEER_DISCONNECTED':
this.message = 'Disconnected from Ambianic Edge device'
this.visibility = true
break
default:
break
}
}
},
watch: {
peerConnectionStatus: function () {
this.setConnectionStatusNotification()
}
}
}
</script>

<style>
#snack-message {
padding-top: 5px;
font-size: .9rem;
}
</style>
4 changes: 4 additions & 0 deletions src/views/Timeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,14 @@ import {
NEW_REMOTE_PEER_ID
} from '@/store/mutation-types'
import moment from 'moment'

Vue.use(VueObserveVisibility)
const PAGE_SIZE = 5
export default {
data () {
return {
connectionBarText: '',
connectionBarVisibility: false,
timeline: [],
clearTimeline: true, // flag to clear timeline when Edge Peer ID changes
imageURL: {}, // map[id, fullURL] - maps unique event id to their full thumbnail URLs
Expand All @@ -252,6 +255,7 @@ export default {
},
beforeDestroy () {
this.pnpUnsubscribe()
this.pnpUnsubscribe()
},
components: {
DetectionBoxes: () => import('@/components/DetectionBoxes.vue'),
Expand Down
41 changes: 21 additions & 20 deletions tests/unit/components/navbar.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import Vuetify from 'vuetify'
import VueX from 'vuex'
import VueRouter from 'vue-router'
import NavBar from '@/components/NavBar.vue'
import { PEER_DISCOVER } from '@/store/action-types'
import { pnpStoreModule } from '@/store/pnp'
import { PEER_CONNECTING, PEER_DISCONNECTED } from '@/store/mutation-types'

describe('NavBar', () => {
// global
Expand All @@ -13,7 +14,7 @@ describe('NavBar', () => {
Vue.use(Vuetify) // for shallowshallowMount use
localVue.use(VueX)

let store, state, getters, actions
let store

// global
localVue.use(VueRouter)
Expand All @@ -22,25 +23,10 @@ describe('NavBar', () => {
const router = new VueRouter()

beforeEach(() => {
state = {
pnp: {
peerConnectionStatus: jest.fn()
}
}

getters = {
// ...
}

actions = {
[PEER_DISCOVER] (context) {
}
}

store = new VueX.Store({
state,
getters,
actions
modules: {
pnp: pnpStoreModule
}
})

// using shallowMount with subtree components
Expand Down Expand Up @@ -75,4 +61,19 @@ describe('NavBar', () => {
expect(nav.exists()).toBe(true)
expect(item.length).toBe(5)
})

test('`peerConnectionStatus` changes ConnectionStatusIcon icon', () => {
store.state.pnp.peerConnectionStatus = PEER_DISCONNECTED
expect(wrapper.find('#cloud-off-outline').exists()).toBeTruthy()

store.state.pnp.peerConnectionStatus = PEER_CONNECTING
const newComponent = wrapper = mount(NavBar, {
localVue,
vuetify,
router,
store
})

expect(newComponent.find('#cloud-sync-outline').exists()).toBeTruthy()
})
})
82 changes: 82 additions & 0 deletions tests/unit/components/shared/snackNotification.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Vue from 'vue'
import { mount, createLocalVue } from '@vue/test-utils'
import Vuetify from 'vuetify'
import SnackNotification from '@/components/shared/snackNotification'
import Vuex from 'vuex'
import { pnpStoreModule } from '@/store/pnp'
import { PEER_CONNECTED, PEER_CONNECTING, PEER_DISCONNECTED } from '@/store/mutation-types'

describe('SnackNotification Component', () => {
let wrapper, store
const localVue = createLocalVue()

Vue.use(Vuetify)
Vue.use(Vuex)

const vuetify = new Vuetify()

beforeEach(() => {
store = new Vuex.Store({
modules: {
pnp: pnpStoreModule
}
})

wrapper = mount(SnackNotification, {
localVue,
vuetify,
store
})
})

afterEach(() => {
wrapper.destroy()
})

it('It should contain a `p` element and close icon', () => {
expect(wrapper.find('#snack-message').exists()).toBeTruthy()
expect(wrapper.find('#close-icon').exists()).toBeTruthy()
})

it('Displayed text changes with `peerConnectionStatus` state', () => {
const connectingText = 'Connecting to Ambianic Edge device'
const connectedText = 'Connected to Ambianic Edge device'
const disconnectedText = 'Disconnected from Ambianic Edge device'

store.state.pnp.peerConnectionStatus = PEER_CONNECTING
const connectingComp = mount(SnackNotification, {
localVue,
vuetify,
store
})
expect(connectingComp.find('#snack-message').text()).toBe(connectingText)

store.state.pnp.peerConnectionStatus = PEER_CONNECTED
const connectedComp = mount(SnackNotification, {
localVue,
vuetify,
store
})
expect(connectedComp.find('#snack-message').text()).toBe(connectedText)

store.state.pnp.peerConnectionStatus = PEER_DISCONNECTED
const disconnectedComp = mount(SnackNotification, {
localVue,
vuetify,
store
})
expect(disconnectedComp.find('#snack-message').text()).toBe(disconnectedText)
})

it('`peerConnectionStatus` status controls SnackNotification visibility', () => {
store.state.pnp.peerConnectionStatus = PEER_CONNECTED

const newComponent = mount(SnackNotification, {
localVue,
vuetify,
store
})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vickywane how about a couple of test cases to cover the behavior of the status snackbar when the user switches between menus. This will help prevent regression on this known issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vickywane did you see this comment?

expect(newComponent.find('#snack-message').exists()).toBeTruthy()
})
})
Loading