Skip to content

Commit

Permalink
Merge pull request #70 from LCOGT/feature/delete-session
Browse files Browse the repository at this point in the history
Feature/delete session
  • Loading branch information
capetillo authored Sep 6, 2024
2 parents 3f5d5af + 9138285 commit a5be9a5
Show file tree
Hide file tree
Showing 10 changed files with 752 additions and 79 deletions.
485 changes: 441 additions & 44 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/pro-regular-svg-icons": "^6.6.0",
"@fortawesome/vue-fontawesome": "^3.0.6",
"@vitejs/plugin-vue": "^5.1.2",
"bulma": "^1.0.0",
"core-js": "^3.8.3",
"d3-celestial": "^0.7.35",
"flush-promises": "^1.0.2",
"leaflet": "^1.9.4",
"lottie-web-vue": "^2.0.7",
"material-icons": "^1.13.12",
Expand All @@ -43,6 +45,7 @@
"eslint-plugin-n": "^16.6.2",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.25.0",
"jsdom": "^25.0.0",
"vitest": "^2.0.5"
},
"eslintConfig": {
Expand Down
2 changes: 1 addition & 1 deletion public/config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"rtiBridgeUrl": "https://lco-edu-rti-bridge.lco.earth/",
"targetNameUrl": "https://simbad2k.lco.global/",
"configdbUrl": "http://configdb.lco.gtn/",
"demo": true
"demo": false
}
31 changes: 23 additions & 8 deletions src/components/Dashboard/UpcomingBookings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@
import { useRouter } from 'vue-router'
import { ref, computed, onMounted } from 'vue'
import { useSessionsStore } from '../../stores/sessions'
import { useUserDataStore } from '../../stores/userData'
import { useConfigurationStore } from '../../stores/configuration'
import { formatDate, formatTime } from '../../utils/formatTime.js'
import { fetchApiCall } from '../../utils/api.js'
const router = useRouter()
const sessionsStore = useSessionsStore()
const userDataStore = useUserDataStore()
const configurationStore = useConfigurationStore()
// change to bookings and add an icon to show completion
const sortedSessions = computed(() => {
const now = new Date().getTime()
// TODO: Show past sessions for a certain amount of time in a separate section
const twoHoursAgo = now - 120 * 60 * 1000
if (sessionsStore.sessions.results === undefined) {
return []
} else {
return sessionsStore.sessions.results.filter(session => new Date(session.start).getTime() >= twoHoursAgo)
.slice()
.sort((a, b) => new Date(a.start) - new Date(b.start))
}
const sessions = sessionsStore.sessions.results || []
return sessions.filter(session => new Date(session.start).getTime() >= twoHoursAgo)
.slice()
.sort((a, b) => new Date(a.start) - new Date(b.start))
})
const observations = ref([
Expand All @@ -33,6 +35,18 @@ const selectSession = (sessionId) => {
router.push(`/realtime/${sessionId}`)
}
async function deleteSession (sessionId) {
sessionsStore.currentSessionId = sessionId
const token = userDataStore.authToken
await fetchApiCall({
url: configurationStore.observationPortalUrl + `realtime/${sessionId}/`,
method: 'DELETE',
header: { Authorization: `Token ${token}` },
successCallback: sessionsStore.sessions.results = sessionsStore.sessions.results.filter(session => session.id !== sessionId),
failCallback: (error) => { console.error('API call failed with error', error) }
})
}
onMounted(() => {
sessionsStore.fetchSessions()
})
Expand All @@ -45,7 +59,8 @@ onMounted(() => {
<h3 v-else>No Real-Time Sessions Booked</h3>
<div class="table-summary">
<div v-for="session in sortedSessions" :key="session.id">
<div><a @click.prevent="selectSession(session.id)">{{ formatDate(session.start) }}</a></div><div>{{ formatTime(session.start) }}</div>
<div><a @click.prevent="selectSession(session.id)" class="date">{{ formatDate(session.start) }}</a></div><div>{{ formatTime(session.start) }}</div>
<button @click="deleteSession(session.id)" class="deleteButton">x</button>
</div>
</div>
<button class="button red-bg" @click="router.push('/book/realtime')"> Book Slot </button>
Expand Down
44 changes: 29 additions & 15 deletions src/components/Images/MyGallery.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@ const loading = ref(true)
const filteredSessions = computed(() => {
const now = new Date()
const cutoffTime = new Date(now.getTime() - 16 * 60 * 1000)
const sessions = sessionsStore.sessions.results || []
return sessions
const filtered = sessions
.filter(session => new Date(session.start) < cutoffTime)
.sort((a, b) => new Date(b.start) - new Date(a.start))
return filtered
})
const getThumbnails = async (sessionId) => {
Expand All @@ -42,41 +41,56 @@ const getThumbnails = async (sessionId) => {
}
loading.value = false
},
failCallback: console.error
failCallback: (error) => {
console.error('Error fetching thumbnails for session:', sessionId, error)
loading.value = false
}
})
}
onMounted(() => {
// Fetch thumbnails for all sessions in filteredSessions
filteredSessions.value.forEach(session => {
thumbnailsMap.value[session.id] = []
getThumbnails(session.id)
})
})
const sessionsWithThumbnails = computed(() => {
return filteredSessions.value.filter(session => thumbnailsMap.value[session.id] && thumbnailsMap.value[session.id].length > 0)
if (loading.value) return []
const sessions = filteredSessions.value.filter(session => thumbnailsMap.value[session.id] && thumbnailsMap.value[session.id].length > 0)
return sessions
})
</script>

<template>
<template v-if="loading">
<v-progress-circular indeterminate color="white" class="loading"/>
</template>
<div class="container" v-for="obs in sessionsWithThumbnails" :key="obs.id">
<h3>{{ formatDate(obs.start) }}</h3>
<div class="columns is-multiline">
<div
class="column is-one-quarter-desktop is-half-tablet"
v-for="(thumbnailUrl, i) in thumbnailsMap[obs.id]"
:key="obs.id + '-' + i">
<figure class="image is-square">
<img :src="thumbnailUrl" class="thumbnail" />
</figure>
<div class="container">
<div v-for="obs in sessionsWithThumbnails" :key="obs.id">
<h3 class="startTime">{{ formatDate(obs.start) }}</h3>
<div class="columns is-multiline">
<div
class="column is-one-quarter-desktop is-half-tablet"
v-for="(thumbnailUrl, i) in thumbnailsMap[obs.id]"
:key="obs.id + '-' + i">
<figure class="image is-square">
<img :src="thumbnailUrl" class="thumbnail" />
</figure>
</div>
</div>
</div>
</div>
</template>

<style scoped>
.loading-spinner {
text-align: center;
}
</style>

<style scoped>
.loading {
position: relative;
Expand Down
12 changes: 1 addition & 11 deletions src/components/Views/ImagesView.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
<script setup>
import MyGallery from '../Images/MyGallery.vue'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { ref } from 'vue'
// TO DO: get images from api and sort them by
const observations = ref([
{ id: 1, date: 'May 19, 2024', telescope: 'Maui, LCO', num: 3 },
{ id: 2, date: 'April 3, 2024', telescope: 'Queue', num: 4 },
{ id: 3, date: 'March 13, 2024', telescope: 'Maui, LCO', num: 6 },
{ id: 4, date: 'March 12, 2024', telescope: 'Queue', num: 4 }
])
</script>

<template>
<div class="container" v-for="obs in observations" :key="obs.id">
<div class="container">
<MyGallery />
</div>
</template>
139 changes: 139 additions & 0 deletions src/tests/integration/components/myGallery.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import flushPromises from 'flush-promises'
import MyGallery from '../../../components/Images/MyGallery.vue'
import { fetchApiCall } from '../../../utils/api.js'
import { formatDate } from '../../../utils/formatTime.js'
import { createTestStores } from '../../../utils/testUtils.js'

// Mock the fetchApiCall function
vi.mock('../../../utils/api.js', () => ({
fetchApiCall: vi.fn()
}))

// Creates a fresh component instance so that each test is isolated
// Avoids cross-test pollution and ensures a clean slate for each test
function createComponent () {
// Initialize the stores using the shared utility
const { pinia, sessionsStore } = createTestStores()

// Set up initial state for the sessions store
sessionsStore.sessions = {
results: [
{ id: 'session1', start: '2024-08-01T12:00:00Z' },
{ id: 'session2', start: '2024-08-01T12:30:00Z' }
]
}

// Mount the component with the pinia stores provided
return mount(MyGallery, {
global: {
plugins: [pinia]
}
})
}

describe('MyGallery.vue', () => {
beforeEach(() => {
// Reset all mocks before each test
vi.resetAllMocks()
})

it('renders a loading indicator on mounted', () => {
const wrapper = createComponent()
expect(wrapper.find('.loading').exists()).toBe(true)
})

it('fetches thumbnails for each session on mount', async () => {
fetchApiCall.mockImplementation(({ url, successCallback }) => {
if (url.includes('session1')) {
successCallback({
results: [{ url: 'http://mock-image.com/image1.jpg' }]
})
} else if (url.includes('session2')) {
successCallback({
results: [{ url: 'http://mock-image.com/image2.jpg' }]
})
}
})

const wrapper = createComponent()
await wrapper.vm.$nextTick()

// Two API calls are made - one for each session ID - to fetch images for both sessions
expect(fetchApiCall).toHaveBeenCalledTimes(2)
expect(fetchApiCall).toHaveBeenCalledWith(
expect.objectContaining({
url: 'http://mock-api.com/thumbnails/?observation_id=session1&size=large'
})
)
expect(fetchApiCall).toHaveBeenCalledWith(
expect.objectContaining({
url: 'http://mock-api.com/thumbnails/?observation_id=session2&size=large'
})
)
})

it('renders sessions with thumbnails after loading', async () => {
fetchApiCall.mockImplementation(({ url, successCallback }) => {
if (url.includes('session1')) {
successCallback({
results: [{ url: 'http://mock-image.com/image1.jpg' }]
})
} else if (url.includes('session2')) {
successCallback({
results: [{ url: 'http://mock-image.com/image2.jpg' }]
})
}
})

const wrapper = createComponent()
await wrapper.vm.$nextTick()

expect(wrapper.find('.loading').exists()).toBe(false)

const allSessionThumbnails = wrapper.findAll('.thumbnail')
expect(allSessionThumbnails.length).toBe(2)
expect(allSessionThumbnails[0].attributes('src')).toBe('http://mock-image.com/image2.jpg')
expect(allSessionThumbnails[1].attributes('src')).toBe('http://mock-image.com/image1.jpg')
})

it('renders sessions in the correct descending order', async () => {
fetchApiCall.mockImplementation(({ url, successCallback }) => {
if (url.includes('session1')) {
successCallback({
results: [{ url: 'http://mock-image.com/image1.jpg' }]
})
} else if (url.includes('session2')) {
successCallback({
results: [{ url: 'http://mock-image.com/image2.jpg' }]
})
}
})

const wrapper = createComponent()
await flushPromises()
await new Promise((resolve) => setTimeout(resolve, 100))

const sessionElements = wrapper.findAll('h3.startTime')
expect(sessionElements.length).toBe(2)

const mostRecentSession = sessionElements[0]
const earlierSession = sessionElements[1]

expect(mostRecentSession.text()).toContain(formatDate('2024-08-01T12:30:00Z'))
expect(earlierSession.text()).toContain(formatDate('2024-08-01T12:00:00Z'))
})

it('does not render sessions without thumbnails', async () => {
fetchApiCall.mockImplementationOnce(({ successCallback }) => {
successCallback({ results: [] })
})

const wrapper = createComponent()
await flushPromises()

const thumbnails = wrapper.findAll('.thumbnail')
expect(thumbnails.length).toBe(0)
})
})
Loading

0 comments on commit a5be9a5

Please sign in to comment.