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
# copy over all code and build
COPY . .
RUN npm run build
USER appuser
# Stage 2
# 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-security.conf /etc/nginx/snippets/nginx-security.conf
......
# launchboard
Static site for https://launchboard.dso.mil (coming soon)
(staging available now: https://launchboard.staging.dso.mil)
Front-end for https://launchboard.apps.dso.mil
(staging: https://launchboard.staging.dso.mil)
## Project setup
......
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-Content-Type-Options "nosniff";
......
This diff is collapsed.
......@@ -21,7 +21,7 @@
"axios": "^0.21.1",
"core-js": "^3.6.4",
"downloadjs": "^1.4.7",
"lodash": "^4.17.20",
"lodash": "4.17.21",
"moment": "^2.29.1",
"vue": "^2.6.11",
"vue-apexcharts": "^1.6.0",
......@@ -157,11 +157,11 @@
"http-proxy": "^1.18.1",
"hosted-git-info": "^3.0.8",
"ini": "^1.3.6",
"is-svg": "^4.3.1",
"lodash": "^4.17.20",
"lodash": "4.17.21",
"minimist": "^1.2.5",
"node-notifier": "^9.0.0",
"normalize-url": "^4.5.1",
"postcss": "^7.0.36",
"serialize-javascript": "^3.1.0",
"sockjs": "^0.3.21",
"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 @@
<!-- TASK: change meta urls to prod once it is available -->
<!-- Open Graph / Facebook -->
<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:description"
......@@ -26,15 +26,12 @@
/>
<meta
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 -->
<meta property="twitter:card" content="summary_large_image" />
<meta
property="twitter:url"
content="https://launchboard.staging.dso.mil/"
/>
<meta property="twitter:url" content="https://launchboard.apps.dso.mil/" />
<meta property="twitter:title" content="Platform One: Launchboard" />
<meta
property="twitter:description"
......@@ -42,7 +39,7 @@
/>
<meta
property="twitter:image"
content="https://launchboard.staging.dso.mil/static/meta-logo.png"
content="https://launchboard.apps.dso.mil/static/meta-logo.png"
/>
<!-- Favicon -->
......@@ -63,7 +60,11 @@
sizes="16x16"
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" />
</head>
<body>
......
......@@ -3,7 +3,7 @@
<TutorialTooltip />
<ErrorDialog />
<NavBar />
<v-main id="main-container" fluid>
<v-main id="main-container" class="pt-0" fluid>
<router-view id="main-content" />
</v-main>
<Footer />
......@@ -26,7 +26,8 @@ export default {
syncDarkMode() {
// make sure vuetify matches the current preference
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 {
<style lang="scss">
#app {
min-width: 320px;
&.theme--dark {
#main-container {
background-image: url("~@/assets/images/tech-bg.jpg");
background-attachment: fixed;
background-repeat: no-repeat;
background-size: $lg-breakpoint 100%;
background-position: center center;
// stretch bg on lg screens
@include lg-up {
......@@ -67,6 +66,7 @@ export default {
#main-content {
height: 100%;
padding-bottom: 32px;
padding-top: 18px;
}
a {
......
......@@ -32,11 +32,10 @@ export default {
},
async updateTeam(team) {
const { name, description, capacity } = team;
const { name, description } = team;
return HTTP.put(`/teams/${team.id}`, {
name,
description,
capacity,
});
},
async updateTeamMembers(team) {
......
......@@ -26,6 +26,7 @@ export default {
return HTTP.get("/users/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 {
}),
methods: {
setInputElementFocus,
init() {},
cancel() {
this.$emit("cancel");
this.init();
},
addCourse() {
this.$emit("add", this.toAdd);
this.init();
},
},
};
......
......@@ -36,22 +36,6 @@
]"
></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
v-model="leads"
ref="UserSelect"
......@@ -121,14 +105,11 @@ export default {
}
},
methods: {
init() {},
cancel() {
this.$emit("cancel");
this.init();
},
add() {
this.$emit("add", this.toAdd, this.leads);
this.init();
},
edit() {
// merge members and leads
......@@ -148,7 +129,6 @@ export default {
this.toAdd.members = [...members.values()];
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 {
}
}
}
.burger .burger-lines,
.burger .burger-lines:after,
.burger .burger-lines:before {
......@@ -59,13 +58,29 @@ export default {
content: "";
width: 100%;
border-radius: 0.25em;
background-color: white;
height: 0.25em;
position: absolute;
-webkit-transform: rotate(0);
-ms-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:after,
......
<template>
<v-container class="curriculum-schedule text-left px-4">
<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-text>Your scheduled courses will appear here.</v-card-text>
</v-card>
......@@ -72,6 +72,11 @@ export default {
components: {
ErrorCard,
},
props: {
setCurriculumSchedule: {
type: Function,
},
},
data: () => ({
loading: false,
registrations: [],
......@@ -81,8 +86,7 @@ export default {
async mounted() {
try {
this.loading = true;
const response = await ScheduleService.getScheduleForUser();
this.registrations = response.registrations;
await this.refreshCurriculumSchedule();
} catch (error) {
this.error = true;
this.$store.commit(SET_ERROR_MESSAGE, error);
......@@ -105,6 +109,28 @@ export default {
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>
......
......@@ -39,7 +39,10 @@ export default {
},
updateCourseMembers: {
type: Function,
default: () => {},
default: () => {
// TODO: confirm this is even being triggered; looks like it's being passed as prop 'update-course-members'
return;
},
},
isCourseSelected: {
type: Boolean,
......
......@@ -95,9 +95,8 @@ export default {
if (!this.$refs.editingElementWrapper) {
return null;
}
const editInputs = this.$refs.editingElementWrapper.getElementsByTagName(
"input"
);
const editInputs =
this.$refs.editingElementWrapper.getElementsByTagName("input");
if (!editInputs || editInputs.length === 0) {
return null;
}
......@@ -133,7 +132,6 @@ export default {
this.$emit("change", this.model);
},
reset() {
// this.updateStyle();
this.hover = false;
this.isEditing = false;
},
......
<template>
<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-title>Seat Metrics</v-card-title>
<v-card-text>Seat metrics will appear here.</v-card-text>
......@@ -12,6 +12,7 @@
type="donut"
:options="chartOptions"
:series="series"
height="300"
></apexchart>
</div>
</v-container>
......@@ -26,18 +27,22 @@ import { SET_ERROR_MESSAGE } from "@/store/mutation-types";
import moment from "moment";
import ErrorMessage from "@/components/APIErrorCard";
export default {
props: ["incomingMonth", "incomingYear", "monthMetric"],
components: {
apexchart,
ErrorMessage,
},
props: {
incomingYear: Number,
incomingMonth: Number,
loadingMetric: String,
setMetric: String,
},
data: () => ({
errorMessage: null,
error: false,
month: null,
year: null,
date: null,
loading: false,
meta: [],
emptyString: false,
series: [],
......@@ -51,7 +56,7 @@ export default {
pie: {
customScale: 0.7,
donut: {
size: "83%",
size: "85%",
width: 40,
},
},
......@@ -65,12 +70,14 @@ export default {
},
legend: {
position: "right",
fontSize: "14px",
fontSize: "18px",
width: 250,
offsetY: 80,
offsetX: -80,
offsetY: 30,
offsetX: 60,
markers: {
shape: "square",
radius: 0,
offsetY: 35,
},
labels: {
colors: null,
......@@ -81,7 +88,6 @@ export default {
},
}),
async beforeMount() {
try {
if (this.$props.incomingMonth === -1) {
// current month is january, need december of last year
this.date = moment("01", "MM").subtract(1, "month").format("YYYY-MM");
......@@ -101,7 +107,7 @@ export default {
.add(1, "month")
.format("YYYY");
}
this.loading = true;
this.params = {
registeredCount: true,
dateFrom: this.year + "-" + this.month + "-01",
......@@ -113,8 +119,23 @@ export default {
moment(this.year + "-" + this.month + "-", "YYYY-MM-").daysInMonth(),
};
const response = await MetricsService.capacityMetrics(this.params);
await this.refreshSeatMetrics();
},
methods: {
setSeatMetricsLoading(state) {
this.$store.commit(this.$props.setMetric, state);
},
async refreshSeatMetrics() {
this.setSeatMetricsLoading(true);
this.error = false;
this.emptyString = false;
// Workaround for apexchart not refreshing labels
const chartOptions = { ...this.chartOptions };
this.chartOptions = {};
try {
this.series = [];
const response = await MetricsService.capacityMetrics(this.params);
if (response.metrics.totalCourses === 0) {
this.emptyClass = true;
}
......@@ -150,36 +171,45 @@ export default {
response.metrics.totalCapacity) *
100
);
// Workaround for apexchart not refreshing labels
const labels = [];
this.series = this.series.map((x) => Math.round(x));
this.chartOptions.labels.push(" " + this.series[0] + "%-Reserved");
this.chartOptions.labels.push(" " + this.series[1] + "%-Open");
labels.push(" " + this.series[0] + "%-Reserved");
labels.push(" " + this.series[1] + "%-Open");
chartOptions.labels = labels;
}
if (this.$props.monthMetric === "past") {
this.chartOptions.labels.push(" " + this.series[0] + "%-Completed");
this.chartOptions.labels.push(" " + this.series[1] + "%-Incomplete");
this.chartOptions.labels.push(" " + this.series[2] + "%-Open");
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") {
this.chartOptions.labels.push(
chartOptions.labels.push(
" " + this.series[0] + "%-Reserved (present)"
);
this.chartOptions.labels.push(
chartOptions.labels.push(
" " + this.series[1] + "%-Incomplete (absent)"
);
this.chartOptions.labels.push(" " + this.series[2] + "%-Open");
chartOptions.labels.push(" " + this.series[2] + "%-Open");
}
if (this.$vuetify.theme.dark) {
this.chartOptions.legend.labels.colors = "white";
chartOptions.legend.labels.colors = "white";
} else {
this.chartOptions.legend.labels.colors = "black";
chartOptions.legend.labels.colors = "black";
}
} catch (error) {
} catch (e) {
this.error = true;
this.$store.commit(SET_ERROR_MESSAGE, error);
this.errorMessage = error;
console.error(error);
} finally {
this.loading = false;
this.$store.commit(SET_ERROR_MESSAGE, e);
this.errorMessage = e;
}
this.chartOptions = chartOptions;
this.setSeatMetricsLoading(false);
},
},
computed: {
getLoadingStatus() {
return this.$store.state.loading[this.$props.loadingMetric];
},
},
};
</script>
......@@ -188,10 +218,14 @@ export default {
.curriculum-schedule {
position: relative;
}
.apexcharts-legend {
right: 50px !important;
}
#chart {
margin-left: 130px;
align-items: center;
justify-items: center;
align-items: left;
justify-items: left;
background-color: transparent;
}
</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