UNCLASSIFIED

Commit 79a95b8f authored by graham.smith's avatar graham.smith
Browse files

Merge branch 'BULL-797' into 'master'

BULL-797: Update LB from code.il2

See merge request !130
parents 83f88ca7 062d988f
...@@ -9,10 +9,11 @@ RUN npm ci ...@@ -9,10 +9,11 @@ RUN npm ci
# copy over all code and build # copy over all code and build
COPY . . COPY . .
RUN npm run build RUN npm run build
USER appuser
# Stage 2 # Stage 2
# final image build # final image build
FROM registry.il2.dso.mil/platform-one/devops/pipeline-templates/base-image/harden-nginx-19:1.19.2 FROM registry.il2.dso.mil/platform-one/devops/pipeline-templates/base-image/harden-nginx-20:1.20.1
COPY nginx/nginx.conf /etc/nginx/nginx.conf COPY nginx/nginx.conf /etc/nginx/nginx.conf
COPY nginx/nginx-security.conf /etc/nginx/snippets/nginx-security.conf COPY nginx/nginx-security.conf /etc/nginx/snippets/nginx-security.conf
......
# launchboard # launchboard
Static site for https://launchboard.dso.mil (coming soon) Front-end for https://launchboard.apps.dso.mil
(staging available now: https://launchboard.staging.dso.mil) (staging: https://launchboard.staging.dso.mil)
## Project setup ## Project setup
......
add_header X-Frame-Options "DENY"; add_header X-Frame-Options "DENY";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' www.google-analytics.com; style-src 'self' 'nonce-bGF1bmNoYm9hcmQtbm9uY2U=' www.google-analytics.com; img-src 'self'; connect-src 'self' www.google-analytics.com; font-src 'self'; object-src 'self'; media-src 'self'; frame-src 'none'; form-action 'self'; frame-ancestors 'none';" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' www.google-analytics.com; style-src 'self' 'nonce-bGF1bmNoYm9hcmQtbm9uY2U=' www.google-analytics.com; img-src 'self'; connect-src 'self' www.google-analytics.com; font-src 'self'; object-src 'self'; media-src 'self'; manifest-src 'self' 'nonce-bGF1bmNoYm9hcmQtbm9uY2U=' *.dso.mil; frame-src 'none'; form-action 'self'; frame-ancestors 'none';" always;
add_header X-XSS-Protection "1; mode=block"; add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff"; add_header X-Content-Type-Options "nosniff";
......
This diff is collapsed.
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
"axios": "^0.21.1", "axios": "^0.21.1",
"core-js": "^3.6.4", "core-js": "^3.6.4",
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",
"lodash": "^4.17.20", "lodash": "4.17.21",
"moment": "^2.29.1", "moment": "^2.29.1",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-apexcharts": "^1.6.0", "vue-apexcharts": "^1.6.0",
...@@ -157,11 +157,11 @@ ...@@ -157,11 +157,11 @@
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"hosted-git-info": "^3.0.8", "hosted-git-info": "^3.0.8",
"ini": "^1.3.6", "ini": "^1.3.6",
"is-svg": "^4.3.1", "lodash": "4.17.21",
"lodash": "^4.17.20",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"node-notifier": "^9.0.0", "node-notifier": "^9.0.0",
"normalize-url": "^4.5.1", "normalize-url": "^4.5.1",
"postcss": "^7.0.36",
"serialize-javascript": "^3.1.0", "serialize-javascript": "^3.1.0",
"sockjs": "^0.3.21", "sockjs": "^0.3.21",
"ssri": "^8.0.1", "ssri": "^8.0.1",
......
# Full name of the system.
system-name: Launchboard Front End
# Shortname or abbreviation.
short-name: Launchboard FE
# System version or release number.
version: 1.0
# Operational Environments: production, test, research and development, tactical, deployed, or other.
environment: production
# Authorization Status: CtF
Status: Active
# Assessment Date. Identify the date of last assessment per Continuous ATO (cATO).
Review-date: TBD
# Security Re-Assess Interval. Period for re-assessment after updates
Assess-interval: As needed.
# Authorization boundary diagram. Include URL.
System-architecture: https://confluence.il2.dso.mil/display/P1/Launchboard
# Confidentiality, Integrity, Availability M,M,M
# Classification: ie. UNCLASSIFIED, SECRET//NOFORN, TS/SCI
# Personally Identifiable Information: Y/N. Assessment team can help data types
Categorization:
Confidentiality: M
Integrity: M
Availability: M
Classification-Level: UNCLASSIFIED
PII: None
Data-types:
-
Deployment-sites:
- https://launchboard.apps.dso.mil
Programming-Languages:
- JavaScript
Dependencies:
- AWS, IL2 partybus staging, and mission application production cluster. For rendering of status, all apis for systems in P1/
- see ../package.json for specific Launchboard FE dependencies
Databases:
- mysql
External-Systems:
- GitLab (code.il2.dso.mil)
Role-Identification:
Authorizing-Official:
name: Daniel Holtzman
title: Authorizing Official
org: Air Force Command & Control
email: daniel.holtzman.1@us.af.mil
phone: 781-225-1118
System-Owner:
name: Nic Chaillan
title: Air Force Chief Software Officer
org: U.S. Air Force
email: nicolas.m.chaillan.civ@mail.mil
phone: 703-693-4740
Chief-Information-Security-Officer:
name: Matt Huston
title: CISO
org: AFLCMC/HNCP
email: matthew.huston@afwerx.af.mil
phone: 650-933-4121
Product-Manager:
name: Erica Westendorf
title: Project Manager
org: Platform1
email: westendorf_erica@bah.com
phone: 571-405-8544
- **Kick-off Date:** 0XNOV2020
- **Inception Date:** JUL2020
- **MVP Date:** estimated 1/16/2020
#### Vision
A secure and fully owned SRE tool for Platform One developers and development teams
#### Strategy
Launchboard is a dashboard serving as a collective-resource where users can access relevant information pertaining to their projects and role. This allows for multiple agencies involved to go off of one centralized database rather than information living in unsecured documents across multiple locations. The FE ties into APIs for the various hardened tools Platform One uses.
#### Summary
SRE tool
### Goals and Metrics
Daily users, bounce rate, system uptime, contact from users/contact form usage, logs, logged in users
**Release Focus**
- MVP focus is on a one stop shop for users to see status of projects, onboarding, workshops, and teams.
**Release Functionality**
- Daily release of updates to content to IL2 only for now
**System Integration Considerations**
- Our releases will not affect any other system at P1.
- Integration with P1 SSO
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<!-- TASK: change meta urls to prod once it is available --> <!-- TASK: change meta urls to prod once it is available -->
<!-- Open Graph / Facebook --> <!-- Open Graph / Facebook -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="https://launchboard.staging.dso.mil/" /> <meta property="og:url" content="https://launchboard.apps.dso.mil/" />
<meta property="og:title" content="Platform One: Launchboard" /> <meta property="og:title" content="Platform One: Launchboard" />
<meta <meta
property="og:description" property="og:description"
...@@ -26,15 +26,12 @@ ...@@ -26,15 +26,12 @@
/> />
<meta <meta
property="og:image" property="og:image"
content="https://https://launchboard.staging.dso.mil/static/meta-logo.png" content="https://https://launchboard.apps.dso.mil/static/meta-logo.png"
/> />
<!-- Twitter --> <!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:card" content="summary_large_image" />
<meta <meta property="twitter:url" content="https://launchboard.apps.dso.mil/" />
property="twitter:url"
content="https://launchboard.staging.dso.mil/"
/>
<meta property="twitter:title" content="Platform One: Launchboard" /> <meta property="twitter:title" content="Platform One: Launchboard" />
<meta <meta
property="twitter:description" property="twitter:description"
...@@ -42,7 +39,7 @@ ...@@ -42,7 +39,7 @@
/> />
<meta <meta
property="twitter:image" property="twitter:image"
content="https://launchboard.staging.dso.mil/static/meta-logo.png" content="https://launchboard.apps.dso.mil/static/meta-logo.png"
/> />
<!-- Favicon --> <!-- Favicon -->
...@@ -63,7 +60,11 @@ ...@@ -63,7 +60,11 @@
sizes="16x16" sizes="16x16"
href="./favicon/favicon-16x16.png" href="./favicon/favicon-16x16.png"
/> />
<link rel="manifest" href="./favicon/site.webmanifest" /> <link
rel="manifest"
href="./favicon/site.webmanifest"
nonce="bGF1bmNoYm9hcmQtbm9uY2U="
/>
<link rel="stylesheet" href="./font.css" /> <link rel="stylesheet" href="./font.css" />
</head> </head>
<body> <body>
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<TutorialTooltip /> <TutorialTooltip />
<ErrorDialog /> <ErrorDialog />
<NavBar /> <NavBar />
<v-main id="main-container" fluid> <v-main id="main-container" class="pt-0" fluid>
<router-view id="main-content" /> <router-view id="main-content" />
</v-main> </v-main>
<Footer /> <Footer />
...@@ -26,7 +26,8 @@ export default { ...@@ -26,7 +26,8 @@ export default {
syncDarkMode() { syncDarkMode() {
// make sure vuetify matches the current preference // make sure vuetify matches the current preference
if (this.$store && this.$store.state.userPreferences.userPreference) { if (this.$store && this.$store.state.userPreferences.userPreference) {
this.$vuetify.theme.dark = this.$store.state.userPreferences.userPreference.darkMode; this.$vuetify.theme.dark =
this.$store.state.userPreferences.userPreference.darkMode;
} }
}, },
}, },
...@@ -40,14 +41,12 @@ export default { ...@@ -40,14 +41,12 @@ export default {
<style lang="scss"> <style lang="scss">
#app { #app {
min-width: 320px; min-width: 320px;
&.theme--dark { &.theme--dark {
#main-container { #main-container {
background-image: url("~@/assets/images/tech-bg.jpg"); background-image: url("~@/assets/images/tech-bg.jpg");
background-attachment: fixed; background-attachment: fixed;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: $lg-breakpoint 100%; background-size: $lg-breakpoint 100%;
background-position: center center; background-position: center center;
// stretch bg on lg screens // stretch bg on lg screens
@include lg-up { @include lg-up {
...@@ -67,6 +66,7 @@ export default { ...@@ -67,6 +66,7 @@ export default {
#main-content { #main-content {
height: 100%; height: 100%;
padding-bottom: 32px; padding-bottom: 32px;
padding-top: 18px;
} }
a { a {
......
...@@ -32,11 +32,10 @@ export default { ...@@ -32,11 +32,10 @@ export default {
}, },
async updateTeam(team) { async updateTeam(team) {
const { name, description, capacity } = team; const { name, description } = team;
return HTTP.put(`/teams/${team.id}`, { return HTTP.put(`/teams/${team.id}`, {
name, name,
description, description,
capacity,
}); });
}, },
async updateTeamMembers(team) { async updateTeamMembers(team) {
......
...@@ -26,6 +26,7 @@ export default { ...@@ -26,6 +26,7 @@ export default {
return HTTP.get("/users/preferences"); return HTTP.get("/users/preferences");
}, },
async updateUserPreferences(preferences) { async updateUserPreferences(preferences) {
return HTTP.post("/users/preferences", preferences); const results = await HTTP.post("/users/preferences", preferences);
return results[0];
}, },
}; };
...@@ -207,14 +207,11 @@ export default { ...@@ -207,14 +207,11 @@ export default {
}), }),
methods: { methods: {
setInputElementFocus, setInputElementFocus,
init() {},
cancel() { cancel() {
this.$emit("cancel"); this.$emit("cancel");
this.init();
}, },
addCourse() { addCourse() {
this.$emit("add", this.toAdd); this.$emit("add", this.toAdd);
this.init();
}, },
}, },
}; };
......
...@@ -36,22 +36,6 @@ ...@@ -36,22 +36,6 @@
]" ]"
></v-textarea> ></v-textarea>
<v-text-field
label="Maximum Members"
type="number"
v-model="toAdd.capacity"
hint="* Required"
persistent-hint
required
:rules="[
inputRules.required,
inputRules.positiveWithMessage(
toAdd.capacity,
'Members must be greater than zero'
),
]"
></v-text-field>
<UserSelect <UserSelect
v-model="leads" v-model="leads"
ref="UserSelect" ref="UserSelect"
...@@ -121,14 +105,11 @@ export default { ...@@ -121,14 +105,11 @@ export default {
} }
}, },
methods: { methods: {
init() {},
cancel() { cancel() {
this.$emit("cancel"); this.$emit("cancel");
this.init();
}, },
add() { add() {
this.$emit("add", this.toAdd, this.leads); this.$emit("add", this.toAdd, this.leads);
this.init();
}, },
edit() { edit() {
// merge members and leads // merge members and leads
...@@ -148,7 +129,6 @@ export default { ...@@ -148,7 +129,6 @@ export default {
this.toAdd.members = [...members.values()]; this.toAdd.members = [...members.values()];
this.$emit("edit", this.toAdd); this.$emit("edit", this.toAdd);
this.init();
}, },
}, },
}; };
......
<template>
<v-container class="my-courses filter-input mtext-left px-2">
<v-skeleton-loader
type="paragraph@5"
class="w-100"
:loading="getCourseState"
>
<v-select
:items="courseSelect"
@change="refreshMyCourses()"
v-model="courses"
placeholder="Filter"
light
single-line
hide-details
class="px-0 mb-5 filter"
></v-select>
<ErrorMessage v-if="error" v-bind:errorMessage="errorMessage" />
<v-card v-if="emptyString">
<v-card-title
>There currently are no current or upcoming courses.</v-card-title
>
<v-card-text
>Courses in progress or upcoming will appear here.</v-card-text
>
</v-card>
<v-container class="px-6" v-else>
<div
class="d-flex col-12 py-0 class-container subhead headers text-left"
v-if="$vuetify.breakpoint.smAndUp"
>
<div class="col-3">Courses</div>
<div class="col-6 seat-availability">Seat Availability</div>
</div>
<div
v-for="(item, index) in schedule"
:key="index"
:class="{ last: index == schedule.length - 1 }"
>
<div
v-if="inDateRange(item.startDate, item.endDate)"
class="dot dot-white"
></div>
<div v-else class="dot dot-green"></div>
<div class="d-flex col-12 py-0 class-container">
<div
v-if="inDateRange(item.startDate, item.endDate)"
class="dot-border dot-border-white col-1 d-inline-block"
></div>
<div
v-else
class="dot-border dot-border-green col-1 d-inline-block"
></div>
<div class="col d-inline-block d-flex align-start text-left">
<div>
<router-link
:to="{
name: 'TrainingDetails',
params: { trainingId: item.id },
}"
>
<div class="class-name">{{ item.name }}</div>
</router-link>
<div class="class-info">
{{ item.startDate }} - {{ item.endDate }}
</div>
<div class="class-info">
<div v-if="inDateRange(item.startDate, item.endDate)">
In Progress
</div>
<div v-else>
{{ seatsRemaining(item.capacity, item.registeredCount) }}
remaining seats
</div>
<div v-if="$vuetify.breakpoint.xs">
<div class="container">
<div class="row">
<div class="pt-2 pl-0 col-8">
<v-progress-linear
class="availability-bar"
rounded
background-color="white"
height="8px"
:value="
(100 * item.registeredCount) / item.capacity
"
></v-progress-linear>
</div>
<div
class="white-space-nowrap small-count col-3 small-xs"
>
{{ item.registeredCount }} / {{ item.capacity }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="col-5 d-inline-block"
v-if="$vuetify.breakpoint.smAndUp"
>
<div class="pt-2">
<v-progress-linear
rounded
background-color="white"
height="8px"
:value="(100 * item.registeredCount) / item.capacity"
></v-progress-linear>
</div>
</div>
<div
class="col-2 d-inline-block"
v-if="$vuetify.breakpoint.smAndUp"
>
<div class="white-space-nowrap ml-5 mt-1 small-count">
{{ item.registeredCount }} / {{ item.capacity }}
</div>
</div>
<div
class="col-1 d-inline-block gutter"
v-if="$vuetify.breakpoint.smAndUp"
/>
</div>
</div>
</v-container>
<div class="text-right">
<router-link class="text-uppercase" to="/training">
<span class="small-bold">SEE ALL &nbsp; ></span>
</router-link>
</div>
</v-skeleton-loader>
</v-container>
</template>
<script>
import TrainingService from "@/api/services/training";
import moment from "moment";
import {
SET_ERROR_MESSAGE,
SET_COURSE_LOADING_STATE,
} from "@/store/mutation-types";
import ErrorMessage from "@/components/APIErrorCard";
export default {
components: {
ErrorMessage,
},
data: () => ({
schedule: [],
emptyString: false,
errorMessage: null,
error: false,
courseSelect: [
{ value: "all", text: "All Courses" },
{ value: "inst", text: "Courses I'm Instructing" },
],
courses: null,
}),
computed: {
getCourseState() {
return this.$store.state.loading.courseState;
},
},
async mounted() {
this.params = {
registeredCount: true,
instructorOf: false,
dateFrom: `${moment().format("YYYY-MM-DD")}`,
dateTo: `${moment().add(6, "months").format("YYYY-MM-DD")}`,
sort: "startDate",
pageSize: 4,
};
await this.refreshMyCourses();
},
methods: {
setCourseState(state) {
this.$store.commit(SET_COURSE_LOADING_STATE, state);
},
inDateRange(startDate, endDate) {
return moment().isBetween(startDate, endDate);
},
seatsRemaining(capacity, registered) {
return capacity - registered;
},
async refreshMyCourses() {
this.setCourseState(true);
this.error = false;
this.emptyString = false;
this.params.instructorOf = false;
if (this.courses === "inst") {
this.params.instructorOf = true;
}
try {
const response = await TrainingService.getCourses(this.params);
if (response && response.courses.length > 0) {
this.schedule = response.courses;
} else {
this.emptyString = true;
}
} catch (e) {
this.error = true;
this.$store.commit(SET_ERROR_MESSAGE, e);
this.errorMessage = e;
}
this.setCourseState(false);
},
},
};
</script>
<style lang="scss" scoped>
.small-count {
font-size: 10px;
}
.availability-bar {
width: 100%;
margin-left: 0px;
padding-left: 0px;
}
.small-xs {
margin-top: -8px;
}
.small-bold {
font-size: 11px;
font-weight: bold;
color: #bdc931 !important;
}
.headers {
margin-left: 35px;
}
.class-name {
color: #bdc931;
}
.class-info,
.class-name {
padding-left: 35px;
}
.theme--light .class-name {
color: #0e6b90;
}
.dot-border-green {
border-left: 1px solid #bdc931;
}
.dot-border-white {
border-left: 1px solid white;
}
.theme--light .dot-border-white,
.theme--light .dot-border-green {
border-left: 1px solid #031322;
}
.class-container {
position: relative;
height: 100%;
}
.dot-border {
position: absolute;
top: 28px;
height: 96%;
left: 12px;
}
.last .dot-border {
border-left: none;
}
.dot {
z-index: 3;
position: absolute;
border-radius: 50%;
height: 13px;
width: 13px;
margin-left: 6px;
margin-top: 17px;
}
.dot-white {
border: 1px solid white;
background-color: white;
}
.theme--light .dot-white {
border: 1px solid #031322;
}
.dot-green {
border: 1px solid #bdc931;
background-color: #031322;
}
.theme--light .dot-green {
border: 1px solid #031322;
}
.apexcharts-legend-series {
margin-bottom: 10px !important;
}
.seat-availability {
padding-left: 30px;
}
.filter {
width: 35%;
}
@media only screen and (max-width: 1425px) {
.resText {
font-size: 12px;
}
.seat-availability {
padding-left: 24px;
}
}
@media only screen and (max-width: 1263px) {
.monthlyseats {
margin-top: 25px;
}
.resText {
font-size: 14px;
}
.seat-availability {
padding-left: 44px;
}
}
@media only screen and (max-width: 680px) {
.resText {
font-size: 12px;
}
.seat-availability {
padding-left: 16px;
}
}
@media only screen and (max-width: 600px) {
.class-name {
min-width: 33%;
}
.filter {
width: 75%;
}
}
@media only screen and (max-width: 480px) {
.class-name {
min-width: 42%;
}
}
@media all and (max-width: 400px) {
.curriculum-schedule {
overflow: hidden !important;
.col-10.d-flex.mx-auto {
display: flex;
flex-direction: column;
padding: 0 !important;
margin: 0 !important;
transform: scale(0.8);
}
}
.dot-border {
left: 12px;
}
div.section-content.pa-3.position-relative
> div
> div
> div
> div:nth-child(3)
> div
> div:nth-child(1)
> div
.dot-border {
left: 23px;
}
.dot-border-green {
display: none;
}
}
::v-deep .theme--light .v-progress-linear__background.white {
background-color: #04294a !important;
}
</style>
...@@ -50,7 +50,6 @@ export default { ...@@ -50,7 +50,6 @@ export default {
} }
} }
} }
.burger .burger-lines, .burger .burger-lines,
.burger .burger-lines:after, .burger .burger-lines:after,
.burger .burger-lines:before { .burger .burger-lines:before {
...@@ -59,13 +58,29 @@ export default { ...@@ -59,13 +58,29 @@ export default {
content: ""; content: "";
width: 100%; width: 100%;
border-radius: 0.25em; border-radius: 0.25em;
background-color: white;
height: 0.25em; height: 0.25em;
position: absolute; position: absolute;
-webkit-transform: rotate(0); -webkit-transform: rotate(0);
-ms-transform: rotate(0); -ms-transform: rotate(0);
transform: rotate(0); transform: rotate(0);
} }
.theme--dark {
.v-btn,
.burger .burger-lines,
.burger .burger-lines:after,
.burger .burger-lines:before {
background-color: white;
}
}
.theme--light {
.v-btn,
.burger .burger-lines,
.burger .burger-lines:after,
.burger .burger-lines:before {
background-color: #031322;
}
}
.burger.burger-squeeze .burger-lines, .burger.burger-squeeze .burger-lines,
.burger.burger-squeeze .burger-lines:after, .burger.burger-squeeze .burger-lines:after,
......
<template> <template>
<v-container class="curriculum-schedule text-left px-4"> <v-container class="curriculum-schedule text-left px-4">
<v-skeleton-loader :loading="loading" type="paragraph@5" class="w-100"> <v-skeleton-loader :loading="loading" type="paragraph@5" class="w-100">
<v-card v-if="registrations.length === 0"> <v-card v-if="registrations.length === 0 && !error">
<v-card-title>You currently don't have any courses! </v-card-title> <v-card-title>You currently don't have any courses! </v-card-title>
<v-card-text>Your scheduled courses will appear here.</v-card-text> <v-card-text>Your scheduled courses will appear here.</v-card-text>
</v-card> </v-card>
...@@ -72,6 +72,11 @@ export default { ...@@ -72,6 +72,11 @@ export default {
components: { components: {
ErrorCard, ErrorCard,
}, },
props: {
setCurriculumSchedule: {
type: Function,
},
},
data: () => ({ data: () => ({
loading: false, loading: false,
registrations: [], registrations: [],
...@@ -81,8 +86,7 @@ export default { ...@@ -81,8 +86,7 @@ export default {
async mounted() { async mounted() {
try { try {
this.loading = true; this.loading = true;
const response = await ScheduleService.getScheduleForUser(); await this.refreshCurriculumSchedule();
this.registrations = response.registrations;
} catch (error) { } catch (error) {
this.error = true; this.error = true;
this.$store.commit(SET_ERROR_MESSAGE, error); this.$store.commit(SET_ERROR_MESSAGE, error);
...@@ -105,6 +109,28 @@ export default { ...@@ -105,6 +109,28 @@ export default {
console.error(error); console.error(error);
} }
}, },
async refreshCurriculumSchedule() {
if (this.setCurriculumSchedule) {
this.setCurriculumSchedule(true);
}
this.error = false;
this.emptyString = false;
try {
const response = await ScheduleService.getScheduleForUser();
if (response && response.registrations.length > 0) {
this.registrations = response.registrations;
} else {
this.emptyString = true;
}
} catch (e) {
this.error = true;
this.$store.commit(SET_ERROR_MESSAGE, e);
this.errorMessage = e;
}
if (this.setCurriculumSchedule) {
this.setCurriculumSchedule(false);
}
},
}, },
}; };
</script> </script>
......
...@@ -39,7 +39,10 @@ export default { ...@@ -39,7 +39,10 @@ export default {
}, },
updateCourseMembers: { updateCourseMembers: {
type: Function, type: Function,
default: () => {}, default: () => {
// TODO: confirm this is even being triggered; looks like it's being passed as prop 'update-course-members'
return;
},
}, },
isCourseSelected: { isCourseSelected: {
type: Boolean, type: Boolean,
......
...@@ -95,9 +95,8 @@ export default { ...@@ -95,9 +95,8 @@ export default {
if (!this.$refs.editingElementWrapper) { if (!this.$refs.editingElementWrapper) {
return null; return null;
} }
const editInputs = this.$refs.editingElementWrapper.getElementsByTagName( const editInputs =
"input" this.$refs.editingElementWrapper.getElementsByTagName("input");
);
if (!editInputs || editInputs.length === 0) { if (!editInputs || editInputs.length === 0) {
return null; return null;
} }
...@@ -133,7 +132,6 @@ export default { ...@@ -133,7 +132,6 @@ export default {
this.$emit("change", this.model); this.$emit("change", this.model);
}, },
reset() { reset() {
// this.updateStyle();
this.hover = false; this.hover = false;
this.isEditing = false; this.isEditing = false;
}, },
......
<template> <template>
<v-container class="curriculum-seats text-left px-2"> <v-container class="curriculum-seats text-left px-2">
<v-skeleton-loader :loading="loading" type="paragraph@5" class="w-100"> <v-skeleton-loader type="image" class="w-100" :loading="getLoadingStatus">
<v-card v-if="emptyClass"> <v-card v-if="emptyClass">
<v-card-title>Seat Metrics</v-card-title> <v-card-title>Seat Metrics</v-card-title>
<v-card-text>Seat metrics will appear here.</v-card-text> <v-card-text>Seat metrics will appear here.</v-card-text>
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
type="donut" type="donut"
:options="chartOptions" :options="chartOptions"
:series="series" :series="series"
height="300"
></apexchart> ></apexchart>
</div> </div>
</v-container> </v-container>
...@@ -26,18 +27,22 @@ import { SET_ERROR_MESSAGE } from "@/store/mutation-types"; ...@@ -26,18 +27,22 @@ import { SET_ERROR_MESSAGE } from "@/store/mutation-types";
import moment from "moment"; import moment from "moment";
import ErrorMessage from "@/components/APIErrorCard"; import ErrorMessage from "@/components/APIErrorCard";
export default { export default {
props: ["incomingMonth", "incomingYear", "monthMetric"],
components: { components: {
apexchart, apexchart,
ErrorMessage, ErrorMessage,
}, },
props: {
incomingYear: Number,
incomingMonth: Number,
loadingMetric: String,
setMetric: String,
},
data: () => ({ data: () => ({
errorMessage: null, errorMessage: null,
error: false, error: false,
month: null, month: null,
year: null, year: null,
date: null, date: null,
loading: false,
meta: [], meta: [],
emptyString: false, emptyString: false,
series: [], series: [],
...@@ -51,7 +56,7 @@ export default { ...@@ -51,7 +56,7 @@ export default {
pie: { pie: {
customScale: 0.7, customScale: 0.7,
donut: { donut: {
size: "83%", size: "85%",
width: 40, width: 40,
}, },
}, },
...@@ -65,12 +70,14 @@ export default { ...@@ -65,12 +70,14 @@ export default {
}, },
legend: { legend: {
position: "right", position: "right",
fontSize: "14px", fontSize: "18px",
width: 250, width: 250,
offsetY: 80, offsetY: 30,
offsetX: -80, offsetX: 60,
markers: { markers: {
shape: "square", shape: "square",
radius: 0,
offsetY: 35,
}, },
labels: { labels: {
colors: null, colors: null,
...@@ -81,105 +88,128 @@ export default { ...@@ -81,105 +88,128 @@ export default {
}, },
}), }),
async beforeMount() { async beforeMount() {
try { if (this.$props.incomingMonth === -1) {
if (this.$props.incomingMonth === -1) { // current month is january, need december of last year
// current month is january, need december of last year this.date = moment("01", "MM").subtract(1, "month").format("YYYY-MM");
this.date = moment("01", "MM").subtract(1, "month").format("YYYY-MM"); this.month = moment("01", "MM").subtract(1, "month").format("MM");
this.month = moment("01", "MM").subtract(1, "month").format("MM"); this.year = moment("01-" + "YYYY", "MM-YYYY")
this.year = moment("01-" + "YYYY", "MM-YYYY") .subtract(1, "month")
.subtract(1, "month") .format("YYYY");
.format("YYYY"); } else {
} else { // current month is december, need january of next year
// current month is december, need january of next year this.date = moment(this.$props.incomingMonth, "M")
this.date = moment(this.$props.incomingMonth, "M") .add(1, "month")
.add(1, "month") .format("YYYY-MM");
.format("YYYY-MM"); this.month = moment(this.$props.incomingMonth, "M")
this.month = moment(this.$props.incomingMonth, "M") .add(1, "month")
.add(1, "month") .format("MM");
.format("MM"); this.year = moment(this.$props.incomingMonth, "M")
this.year = moment(this.$props.incomingMonth, "M") .add(1, "month")
.add(1, "month") .format("YYYY");
.format("YYYY"); }
}
this.loading = true;
this.params = {
registeredCount: true,
dateFrom: this.year + "-" + this.month + "-01",
dateTo:
this.year +
"-" +
this.month +
"-" +
moment(this.year + "-" + this.month + "-", "YYYY-MM-").daysInMonth(),
};
const response = await MetricsService.capacityMetrics(this.params); this.params = {
registeredCount: true,
dateFrom: this.year + "-" + this.month + "-01",
dateTo:
this.year +
"-" +
this.month +
"-" +
moment(this.year + "-" + this.month + "-", "YYYY-MM-").daysInMonth(),
};
if (response.metrics.totalCourses === 0) { await this.refreshSeatMetrics();
this.emptyClass = true; },
} methods: {
if ( setSeatMetricsLoading(state) {
this.$props.monthMetric === "present" || this.$store.commit(this.$props.setMetric, state);
this.$props.monthMetric === "past" },
) { async refreshSeatMetrics() {
this.series.push( this.setSeatMetricsLoading(true);
(response.metrics.totalCompleted / response.metrics.totalCapacity) * this.error = false;
100 this.emptyString = false;
);
this.series.push( // Workaround for apexchart not refreshing labels
(response.metrics.totalRegistrations / const chartOptions = { ...this.chartOptions };
response.metrics.totalCapacity) * this.chartOptions = {};
100 try {
); this.series = [];
this.series.push( const response = await MetricsService.capacityMetrics(this.params);
((response.metrics.totalCapacity - if (response.metrics.totalCourses === 0) {
response.metrics.totalRegistrations) / this.emptyClass = true;
response.metrics.totalCapacity) * }
100 if (
); this.$props.monthMetric === "present" ||
this.series = this.series.map((x) => Math.round(x)); this.$props.monthMetric === "past"
} else { ) {
this.series.push( this.series.push(
(response.metrics.totalRegistrations / (response.metrics.totalCompleted / response.metrics.totalCapacity) *
response.metrics.totalCapacity) * 100
100 );
); this.series.push(
this.series.push( (response.metrics.totalRegistrations /
((response.metrics.totalCapacity - response.metrics.totalCapacity) *
response.metrics.totalRegistrations) / 100
response.metrics.totalCapacity) * );
100 this.series.push(
); ((response.metrics.totalCapacity -
this.series = this.series.map((x) => Math.round(x)); response.metrics.totalRegistrations) /
this.chartOptions.labels.push(" " + this.series[0] + "%-Reserved"); response.metrics.totalCapacity) *
this.chartOptions.labels.push(" " + this.series[1] + "%-Open"); 100
} );
if (this.$props.monthMetric === "past") { this.series = this.series.map((x) => Math.round(x));
this.chartOptions.labels.push(" " + this.series[0] + "%-Completed"); } else {
this.chartOptions.labels.push(" " + this.series[1] + "%-Incomplete"); this.series.push(
this.chartOptions.labels.push(" " + this.series[2] + "%-Open"); (response.metrics.totalRegistrations /
} else if (this.$props.monthMetric === "present") { response.metrics.totalCapacity) *
this.chartOptions.labels.push( 100
" " + this.series[0] + "%-Reserved (present)" );
); this.series.push(
this.chartOptions.labels.push( ((response.metrics.totalCapacity -
" " + this.series[1] + "%-Incomplete (absent)" response.metrics.totalRegistrations) /
); response.metrics.totalCapacity) *
this.chartOptions.labels.push(" " + this.series[2] + "%-Open"); 100
} );
if (this.$vuetify.theme.dark) {
this.chartOptions.legend.labels.colors = "white"; // Workaround for apexchart not refreshing labels
} else { const labels = [];
this.chartOptions.legend.labels.colors = "black"; this.series = this.series.map((x) => Math.round(x));
labels.push(" " + this.series[0] + "%-Reserved");
labels.push(" " + this.series[1] + "%-Open");
chartOptions.labels = labels;
}
if (this.$props.monthMetric === "past") {
chartOptions.labels.push(" " + this.series[0] + "%-Completed");
chartOptions.labels.push(" " + this.series[1] + "%-Incomplete");
chartOptions.labels.push(" " + this.series[2] + "%-Open");
} else if (this.$props.monthMetric === "present") {
chartOptions.labels.push(
" " + this.series[0] + "%-Reserved (present)"
);
chartOptions.labels.push(
" " + this.series[1] + "%-Incomplete (absent)"
);
chartOptions.labels.push(" " + this.series[2] + "%-Open");
}
if (this.$vuetify.theme.dark) {
chartOptions.legend.labels.colors = "white";
} else {
chartOptions.legend.labels.colors = "black";
}
} catch (e) {
this.error = true;
this.$store.commit(SET_ERROR_MESSAGE, e);
this.errorMessage = e;
} }
} catch (error) { this.chartOptions = chartOptions;
this.error = true; this.setSeatMetricsLoading(false);
this.$store.commit(SET_ERROR_MESSAGE, error); },
this.errorMessage = error; },
console.error(error); computed: {
} finally { getLoadingStatus() {
this.loading = false; return this.$store.state.loading[this.$props.loadingMetric];
} },
}, },
}; };
</script> </script>
...@@ -188,10 +218,14 @@ export default { ...@@ -188,10 +218,14 @@ export default {
.curriculum-schedule { .curriculum-schedule {
position: relative; position: relative;
} }
.apexcharts-legend {
right: 50px !important;
}
#chart { #chart {
margin-left: 130px; align-items: left;
align-items: center; justify-items: left;
justify-items: center;
background-color: transparent; background-color: transparent;
} }
</style> </style>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment