| <template> |
| <div> |
| <div class="row"> |
| <div class="col"> |
| <h1 class="h4 mb-4">Experiment Statistics</h1> |
| </div> |
| </div> |
| <b-card header="Load experiment details" no-body> |
| <b-tabs card> |
| <b-tab title="By Experiment ID" active> |
| <b-card-text> |
| <b-form-group> |
| <b-input-group> |
| <b-form-input |
| v-model.trim="experimentId" |
| placeholder="Experiment ID" |
| @keydown.native.enter=" |
| experimentId && showExperimentDetails(experimentId) |
| " |
| /> |
| <b-input-group-append> |
| <b-button |
| :disabled="!experimentId" |
| @click="showExperimentDetails(experimentId)" |
| variant="primary" |
| >Load</b-button |
| > |
| </b-input-group-append> |
| </b-input-group> |
| </b-form-group> |
| </b-card-text> |
| </b-tab> |
| <b-tab title="By Job ID"> |
| <b-card-text> |
| <b-form-group> |
| <b-input-group> |
| <b-form-input |
| v-model.trim="jobId" |
| placeholder="Job ID" |
| @keydown.native.enter=" |
| jobId && showExperimentDetailsForJobId(jobId) |
| " |
| /> |
| <b-input-group-append> |
| <b-button |
| :disabled="!jobId" |
| @click="showExperimentDetailsForJobId(jobId)" |
| variant="primary" |
| >Load</b-button |
| > |
| </b-input-group-append> |
| </b-input-group> |
| </b-form-group> |
| </b-card-text> |
| </b-tab> |
| </b-tabs> |
| </b-card> |
| <b-card no-body> |
| <b-tabs card v-model="activeTabIndex" ref="tabs"> |
| <b-tab :title="selectedExperimentsTabTitle"> |
| <div class="row"> |
| <div class="col"> |
| <b-card header="Filter Options"> |
| <b-input-group class="w-100 mb-2"> |
| <b-input-group-prepend is-text> |
| <i class="fa fa-calendar-week" aria-hidden="true"></i> |
| </b-input-group-prepend> |
| <flat-pickr |
| :value="dateRange" |
| :config="dateConfig" |
| @on-change="dateRangeChanged" |
| class="form-control" |
| /> |
| <b-input-group-append> |
| <b-button |
| @click="getPast24Hours" |
| variant="outline-secondary" |
| >Past 24 Hours</b-button |
| > |
| <b-button @click="getPastWeek" variant="outline-secondary" |
| >Past Week</b-button |
| > |
| </b-input-group-append> |
| </b-input-group> |
| <b-dropdown text="Add Filters" class="mb-2"> |
| <b-dropdown-item |
| v-if="!usernameFilterEnabled" |
| @click="usernameFilterEnabled = true" |
| >Username</b-dropdown-item |
| > |
| <b-dropdown-item |
| v-if="!applicationNameFilterEnabled" |
| @click="applicationNameFilterEnabled = true" |
| >Application Name</b-dropdown-item |
| > |
| <b-dropdown-item |
| v-if="!hostnameFilterEnabled" |
| @click="hostnameFilterEnabled = true" |
| >Hostname</b-dropdown-item |
| > |
| </b-dropdown> |
| <b-input-group v-if="usernameFilterEnabled" class="mb-2"> |
| <b-form-input |
| v-model="usernameFilter" |
| placeholder="Username" |
| @keydown.native.enter="loadStatistics" |
| /> |
| <b-input-group-append> |
| <b-button @click="removeUsernameFilter"> |
| <i class="fa fa-times"></i> |
| <span class="sr-only">Remove username filter</span> |
| </b-button> |
| </b-input-group-append> |
| </b-input-group> |
| <b-input-group v-if="applicationNameFilterEnabled" class="mb-2"> |
| <b-form-select |
| v-model="applicationNameFilter" |
| :options="applicationNameOptions" |
| @input="loadStatistics" |
| > |
| <template slot="first"> |
| <option :value="null" disabled> |
| Select an application to filter on |
| </option> |
| </template> |
| </b-form-select> |
| <b-input-group-append> |
| <b-button @click="removeApplicationNameFilter"> |
| <i class="fa fa-times"></i> |
| <span class="sr-only" |
| >Remove application name filter</span |
| > |
| </b-button> |
| </b-input-group-append> |
| </b-input-group> |
| <b-input-group v-if="hostnameFilterEnabled" class="mb-2"> |
| <b-form-select |
| v-model="hostnameFilter" |
| :options="hostnameOptions" |
| @input="loadStatistics" |
| > |
| <template slot="first"> |
| <option :value="null" disabled> |
| Select compute resource to filter on |
| </option> |
| </template> |
| </b-form-select> |
| <b-input-group-append> |
| <b-button @click="removeHostnameFilter"> |
| <i class="fa fa-times"></i> |
| <span class="sr-only">Remove hostname filter</span> |
| </b-button> |
| </b-input-group-append> |
| </b-input-group> |
| <template slot="footer"> |
| <div class="d-flex justify-content-end"> |
| <b-button |
| @click="loadStatistics" |
| class="ml-auto" |
| variant="primary" |
| >Get Statistics</b-button |
| > |
| </div> |
| </template> |
| </b-card> |
| </div> |
| </div> |
| <div class="row"> |
| <div class="col"> |
| <h2 class="h5 mb-4"> |
| Experiment Statistics from {{ fromTimeDisplay }} to |
| {{ toTimeDisplay }} |
| </h2> |
| </div> |
| </div> |
| <div class="row"> |
| <div class="col-xl-2 col-md-4"> |
| <experiment-statistics-card |
| bg-variant="primary" |
| header-text-variant="white" |
| :count="experimentStatistics.allExperimentCount || 0" |
| title="Total Experiments" |
| @click="selectExperiments('allExperiments')" |
| > |
| <span slot="link-text">All</span> |
| </experiment-statistics-card> |
| </div> |
| <div class="col-xl-2 col-md-4"> |
| <experiment-statistics-card |
| bg-variant="light" |
| :count="experimentStatistics.createdExperimentCount || 0" |
| :states="createdStates" |
| title="Created Experiments" |
| @click="selectExperiments('createdExperiments')" |
| > |
| </experiment-statistics-card> |
| </div> |
| <div class="col-xl-2 col-md-4"> |
| <experiment-statistics-card |
| bg-variant="light" |
| header-text-variant="success" |
| :count="experimentStatistics.runningExperimentCount || 0" |
| :states="runningStates" |
| title="Running Experiments" |
| @click="selectExperiments('runningExperiments')" |
| > |
| </experiment-statistics-card> |
| </div> |
| <div class="col-xl-2 col-md-4"> |
| <experiment-statistics-card |
| bg-variant="success" |
| header-text-variant="white" |
| link-variant="success" |
| :count="experimentStatistics.completedExperimentCount || 0" |
| :states="completedStates" |
| title="Completed Experiments" |
| @click="selectExperiments('completedExperiments')" |
| > |
| </experiment-statistics-card> |
| </div> |
| <div class="col-xl-2 col-md-4"> |
| <experiment-statistics-card |
| bg-variant="warning" |
| header-text-variant="white" |
| link-variant="warning" |
| :count="experimentStatistics.cancelledExperimentCount || 0" |
| :states="canceledStates" |
| title="Cancelled Experiments" |
| @click="selectExperiments('cancelledExperiments')" |
| > |
| </experiment-statistics-card> |
| </div> |
| <div class="col-xl-2 col-md-4"> |
| <experiment-statistics-card |
| bg-variant="danger" |
| header-text-variant="white" |
| link-variant="danger" |
| :count="experimentStatistics.failedExperimentCount || 0" |
| :states="failedStates" |
| title="Failed Experiments" |
| @click="selectExperiments('failedExperiments')" |
| > |
| </experiment-statistics-card> |
| </div> |
| </div> |
| <div class="row" v-if="items.length > 0"> |
| <div class="col"> |
| <b-card> |
| <b-table :fields="fields" :items="items"> |
| <template slot="cell(executionId)" slot-scope="data"> |
| <application-name :application-interface-id="data.value" /> |
| </template> |
| <template slot="cell(resourceHostId)" slot-scope="data"> |
| <compute-resource-name :compute-resource-id="data.value" /> |
| </template> |
| <template slot="cell(creationTime)" slot-scope="data"> |
| <human-date :date="data.value" /> |
| </template> |
| <template slot="cell(experimentStatus)" slot-scope="data"> |
| <experiment-status-badge :status-name="data.value.name" /> |
| </template> |
| <template slot="cell(actions)" slot-scope="data"> |
| <b-link |
| @click="showExperimentDetails(data.item.experimentId)" |
| > |
| View Details |
| <i class="far fa-chart-bar" aria-hidden="true"></i> |
| </b-link> |
| </template> |
| </b-table> |
| </b-card> |
| <pager |
| v-if="experimentStatistics.allExperimentCount > 0" |
| :paginator="experimentStatisticsPaginator" |
| @next="experimentStatisticsPaginator.next()" |
| @previous="experimentStatisticsPaginator.previous()" |
| ></pager> |
| </div> |
| </div> |
| </b-tab> |
| <b-tab |
| v-for="experimentTab in experimentDetailTabs" |
| :key="experimentTab.experiment.experimentId" |
| > |
| <template slot="title"> |
| {{ experimentTab.tabTitle }} |
| <b-link |
| @click=" |
| removeExperimentDetailTab(experimentTab.experiment.experimentId) |
| " |
| class="text-secondary" |
| > |
| <i class="fas fa-times"></i> |
| <span class="sr-only">Close experiment tab</span> |
| </b-link> |
| </template> |
| <experiment-details-view :experiment="experimentTab.experiment" /> |
| </b-tab> |
| </b-tabs> |
| </b-card> |
| </div> |
| </template> |
| <script> |
| import { errors, models, services, utils } from "django-airavata-api"; |
| import { components, notifications } from "django-airavata-common-ui"; |
| import ExperimentStatisticsCard from "./ExperimentStatisticsCard"; |
| import ExperimentDetailsView from "./ExperimentDetailsView"; |
| |
| import moment from "moment"; |
| |
| export default { |
| name: "experiment-statistics-container", |
| data() { |
| //fp_incr sets the time of the date to midnight. |
| //Calculating from today midnight to tomorrow midnight. |
| const fromTime = new Date().fp_incr(0); |
| const toTime = new Date().fp_incr(1); |
| return { |
| experimentStatisticsPaginator: null, |
| selectedExperimentSummariesKey: null, |
| fromTime: fromTime, |
| toTime: toTime, |
| dateRange: [fromTime, toTime], |
| dateConfig: { |
| mode: "range", |
| wrap: true, |
| dateFormat: "Y-m-d", |
| maxDate: new Date().fp_incr(1), |
| }, |
| usernameFilterEnabled: false, |
| usernameFilter: null, |
| applicationNameFilterEnabled: false, |
| applicationNameFilter: null, |
| hostnameFilterEnabled: false, |
| hostnameFilter: null, |
| appInterfaces: null, |
| computeResourceNames: null, |
| experimentDetailTabs: [], |
| experimentId: null, |
| jobId: null, |
| activeTabIndex: 0, |
| }; |
| }, |
| created() { |
| this.loadStatistics(); |
| this.loadApplicationInterfaces(); |
| this.loadComputeResources(); |
| }, |
| components: { |
| ExperimentDetailsView, |
| ExperimentStatisticsCard, |
| "application-name": components.ApplicationName, |
| "compute-resource-name": components.ComputeResourceName, |
| "human-date": components.HumanDate, |
| "experiment-status-badge": components.ExperimentStatusBadge, |
| pager: components.Pager, |
| }, |
| computed: { |
| experimentStatistics() { |
| return this.experimentStatisticsPaginator |
| ? this.experimentStatisticsPaginator.results |
| : {}; |
| }, |
| createdStates() { |
| // TODO: moved to ExperimentStatistics model |
| return [models.ExperimentState.CREATED, models.ExperimentState.VALIDATED]; |
| }, |
| runningStates() { |
| return [ |
| models.ExperimentState.SCHEDULED, |
| models.ExperimentState.LAUNCHED, |
| models.ExperimentState.EXECUTING, |
| ]; |
| }, |
| completedStates() { |
| return [models.ExperimentState.COMPLETED]; |
| }, |
| canceledStates() { |
| return [ |
| models.ExperimentState.CANCELING, |
| models.ExperimentState.CANCELED, |
| ]; |
| }, |
| failedStates() { |
| return [models.ExperimentState.FAILED]; |
| }, |
| fields() { |
| return [ |
| { |
| key: "name", |
| label: "Name", |
| }, |
| { |
| key: "userName", |
| label: "Owner", |
| }, |
| { |
| key: "executionId", |
| label: "Application", |
| }, |
| { |
| key: "resourceHostId", |
| label: "Resource", |
| }, |
| { |
| key: "creationTime", |
| label: "Creation Time", |
| }, |
| { |
| key: "experimentStatus", |
| label: "Status", |
| }, |
| { |
| key: "actions", |
| label: "Actions", |
| }, |
| ]; |
| }, |
| items() { |
| if (this.selectedExperimentSummaries) { |
| return this.selectedExperimentSummaries; |
| } else { |
| return []; |
| } |
| }, |
| fromTimeDisplay() { |
| return moment(this.fromTime).format("MMM Do YYYY"); |
| }, |
| toTimeDisplay() { |
| return moment(this.toTime).format("MMM Do YYYY"); |
| }, |
| selectedExperimentSummaries() { |
| if ( |
| this.selectedExperimentSummariesKey && |
| this.experimentStatistics && |
| this.selectedExperimentSummariesKey in this.experimentStatistics |
| ) { |
| return this.experimentStatistics[this.selectedExperimentSummariesKey]; |
| } else { |
| return []; |
| } |
| }, |
| applicationNameOptions() { |
| if (this.appInterfaces) { |
| const options = this.appInterfaces.map((appInterface) => { |
| return { |
| value: appInterface.applicationInterfaceId, |
| text: appInterface.applicationName, |
| }; |
| }); |
| return utils.StringUtils.sortIgnoreCase(options, (o) => o.text); |
| } else { |
| return []; |
| } |
| }, |
| hostnameOptions() { |
| if (this.computeResourceNames) { |
| const options = this.computeResourceNames.map((name) => { |
| return { |
| value: name.host_id, |
| text: name.host, |
| }; |
| }); |
| return utils.StringUtils.sortIgnoreCase(options, (o) => o.text); |
| } else { |
| return []; |
| } |
| }, |
| selectedExperimentsTabTitle() { |
| if (this.selectedExperimentSummariesKey === "allExperiments") { |
| return "All Experiments"; |
| } else if (this.selectedExperimentSummariesKey === "createdExperiments") { |
| return "Created Experiments"; |
| } else if (this.selectedExperimentSummariesKey === "runningExperiments") { |
| return "Running Experiments"; |
| } else if ( |
| this.selectedExperimentSummariesKey === "completedExperiments" |
| ) { |
| return "Completed Experiments"; |
| } else if ( |
| this.selectedExperimentSummariesKey === "cancelledExperiments" |
| ) { |
| return "Cancelled Experiments"; |
| } else if (this.selectedExperimentSummariesKey === "failedExperiments") { |
| return "Failed Experiments"; |
| } else { |
| return "Experiments"; |
| } |
| }, |
| }, |
| methods: { |
| dateRangeChanged(selectedDates) { |
| [this.fromTime, this.toTime] = selectedDates; |
| if (this.fromTime && this.toTime) { |
| this.loadStatistics(); |
| } |
| }, |
| loadApplicationInterfaces() { |
| return services.ApplicationInterfaceService.list().then( |
| (appInterfaces) => (this.appInterfaces = appInterfaces) |
| ); |
| }, |
| loadComputeResources() { |
| return services.ComputeResourceService.namesList().then( |
| (names) => (this.computeResourceNames = names) |
| ); |
| }, |
| loadStatistics() { |
| const requestData = { |
| fromTime: this.fromTime.toJSON(), |
| toTime: this.toTime.toJSON(), |
| }; |
| if (this.usernameFilterEnabled && this.usernameFilter) { |
| requestData["userName"] = this.usernameFilter; |
| } |
| if (this.applicationNameFilterEnabled && this.applicationNameFilter) { |
| requestData["applicationName"] = this.applicationNameFilter; |
| } |
| if (this.hostnameFilterEnabled && this.hostnameFilter) { |
| requestData["resourceHostName"] = this.hostnameFilter; |
| } |
| return services.ExperimentStatisticsService.get(requestData).then( |
| (stats) => { |
| this.experimentStatisticsPaginator = stats; |
| } |
| ); |
| }, |
| getPast24Hours() { |
| this.fromTime = new Date().fp_incr(0); |
| //this.fromTime = new Date(this.fromTime.setHours(0,0,0)); |
| this.toTime = new Date().fp_incr(1); |
| this.updateDateRange(); |
| }, |
| getPastWeek() { |
| this.fromTime = new Date().fp_incr(-7); |
| this.toTime = new Date().fp_incr(1); |
| this.updateDateRange(); |
| }, |
| updateDateRange() { |
| this.dateRange = [ |
| moment(this.fromTime).format("YYYY-MM-DD"), |
| moment(this.toTime).format("YYYY-MM-DD"), |
| ]; |
| }, |
| daysAgo(days) { |
| return new Date(Date.now() - days * 24 * 60 * 60 * 1000); |
| }, |
| removeUsernameFilter() { |
| this.usernameFilter = null; |
| this.usernameFilterEnabled = false; |
| this.loadStatistics(); |
| }, |
| removeApplicationNameFilter() { |
| this.applicationNameFilter = null; |
| this.applicationNameFilterEnabled = false; |
| this.loadStatistics(); |
| }, |
| removeHostnameFilter() { |
| this.hostnameFilter = null; |
| this.hostnameFilterEnabled = false; |
| this.loadStatistics(); |
| }, |
| async showExperimentDetails(experimentId, tabTitle = null) { |
| const expDetailsIndex = this.getExperimentDetailTabsIndex(experimentId); |
| if (expDetailsIndex >= 0) { |
| // Update tab title in case it is now loaded from a job id and we want |
| // to get the job id in the title |
| if (tabTitle) { |
| this.experimentDetailTabs[expDetailsIndex].tabTitle = tabTitle; |
| } |
| this.selectExperimentDetailsTab(experimentId); |
| } else { |
| try { |
| const exp = await services.ExperimentService.retrieve( |
| { |
| lookup: experimentId, |
| }, |
| { ignoreErrors: true } |
| ); |
| this.experimentDetailTabs.push({ |
| tabTitle: tabTitle || exp.experimentName, |
| experiment: exp, |
| }); |
| this.selectExperimentDetailsTab(experimentId); |
| this.scrollTabsIntoView(); |
| } catch (error) { |
| if (errors.ErrorUtils.isNotFoundError(error)) { |
| notifications.NotificationList.add( |
| new notifications.Notification({ |
| type: "WARNING", |
| message: `No experiment exists with experiment id ${experimentId}`, |
| duration: 5, |
| }) |
| ); |
| } else { |
| utils.FetchUtils.reportError(error); |
| } |
| } |
| } |
| }, |
| async showExperimentDetailsForJobId(jobId) { |
| const searchResults = await services.ExperimentSearchService.list({ |
| [models.ExperimentSearchFields.JOB_ID.name]: jobId, |
| }); |
| if (searchResults.results.length === 0) { |
| notifications.NotificationList.add( |
| new notifications.Notification({ |
| type: "WARNING", |
| message: `No experiment exists with job id ${jobId}`, |
| duration: 5, |
| }) |
| ); |
| } else { |
| if (searchResults.results.length > 1) { |
| notifications.NotificationList.add( |
| new notifications.Notification({ |
| type: "WARNING", |
| message: `More than one experiment matches job id ${jobId}, showing the latest one`, |
| duration: 5, |
| }) |
| ); |
| } |
| this.showExperimentDetails( |
| searchResults.results[0].experimentId, |
| `Job ${jobId}` |
| ); |
| } |
| }, |
| selectExperimentDetailsTab(experimentId) { |
| const expDetailsIndex = this.getExperimentDetailTabsIndex(experimentId); |
| // Note: running this in $nextTick doesn't work, but setTimeout does |
| // (see also https://github.com/bootstrap-vue/bootstrap-vue/issues/1378#issuecomment-345689470) |
| setTimeout(() => { |
| // Add 1 to the index because the first tab has the overall statistics |
| this.activeTabIndex = expDetailsIndex + 1; |
| }, 1); |
| }, |
| getExperimentDetailTabsIndex(experimentId) { |
| return this.experimentDetailTabs.findIndex( |
| (tab) => tab.experiment.experimentId === experimentId |
| ); |
| }, |
| removeExperimentDetailTab(experimentId) { |
| const index = this.getExperimentDetailTabsIndex(experimentId); |
| this.experimentDetailTabs.splice(index, 1); |
| }, |
| scrollTabsIntoView() { |
| this.$refs.tabs.$el.scrollIntoView({ behavior: "smooth" }); |
| }, |
| selectExperiments(experimentSummariesKey) { |
| if ( |
| this.experimentStatisticsPaginator && |
| this.experimentStatisticsPaginator.offset > 0 |
| ) { |
| this.loadStatistics(); |
| } |
| this.selectedExperimentSummariesKey = experimentSummariesKey; |
| }, |
| }, |
| }; |
| </script> |