client/App.vue (467 lines of code) (raw):

<script> // Copyright (c) 2017-2024 Uber Technologies Inc. // // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import { version } from '../package.json'; import logo from './assets/logo.svg'; import { ButtonIcon, FeatureFlag, FlexGrid, FlexGridItem, NewsModal, NotificationBar, SelectInput, } from '~components'; import { ActiveStatus, CrossRegion, Domain, DomainAutocomplete, SettingsModal, } from '~containers'; import { DATE_FORMAT_MMM_D_YYYY, DATE_FORMAT_OPTIONS, ENVIRONMENT_LIST, LOCAL_STORAGE_NEWS_LAST_VIEWED_AT, LOCAL_STORAGE_SETTINGS, NOTIFICATION_TIMEOUT, NOTIFICATION_TYPE_SUCCESS, TIME_FORMAT_12, TIME_FORMAT_OPTIONS, TIMEZONE_LOCAL, TIMEZONE_OPTIONS, } from '~constants'; import { getEnvironment, getEnvironmentList, getLatestNewsItems, parseStringToBoolean, workflowHistoryEventHighlightListAddOrUpdate, } from '~helpers'; import { httpService } from '~services'; export default { components: { 'active-status': ActiveStatus, 'button-icon': ButtonIcon, 'cross-region': CrossRegion, domain: Domain, 'domain-autocomplete': DomainAutocomplete, 'feature-flag': FeatureFlag, 'flex-grid': FlexGrid, 'flex-grid-item': FlexGridItem, 'news-modal': NewsModal, 'notification-bar': NotificationBar, 'select-input': SelectInput, 'settings-modal': SettingsModal, }, data() { const { origin } = window.location; const environmentList = ENVIRONMENT_LIST; return { environment: { list: getEnvironmentList({ environmentList, origin, }), value: getEnvironment({ environmentList, origin, }), }, isSearchingDomain: false, newsLastUpdated: localStorage.getItem(LOCAL_STORAGE_NEWS_LAST_VIEWED_AT), newsItems: [], logo, notification: { message: '', show: false, type: '', timeout: undefined, }, // TODO - refactor App to store these in vuex store settings: { dateFormat: localStorage.getItem(LOCAL_STORAGE_SETTINGS.dateFormat) || DATE_FORMAT_MMM_D_YYYY, dateFormatOptions: DATE_FORMAT_OPTIONS, timeFormat: localStorage.getItem(LOCAL_STORAGE_SETTINGS.timeFormat) || TIME_FORMAT_12, timeFormatOptions: TIME_FORMAT_OPTIONS, timezone: localStorage.getItem(LOCAL_STORAGE_SETTINGS.timezone) || TIMEZONE_LOCAL, timezoneOptions: TIMEZONE_OPTIONS, workflowHistoryEventHighlightList: JSON.parse( localStorage.getItem( LOCAL_STORAGE_SETTINGS.workflowHistoryEventHighlightList ) ) || [], workflowHistoryEventHighlightListEnabled: parseStringToBoolean( localStorage.getItem( LOCAL_STORAGE_SETTINGS.workflowHistoryEventHighlightListEnabled ), true ), }, }; }, beforeDestroy() { clearTimeout(this.notification.timeout); }, async mounted() { await this.fetchLatestNewsItems(); if (this.newsItems.length) { this.$modal.show('news-modal'); } }, methods: { async fetchLatestNewsItems() { const { newsLastUpdated } = this; const response = await httpService.get('/feed.json'); this.newsItems = getLatestNewsItems({ newsLastUpdated, response }); }, globalClick(e) { // Code required for mocha tests to run correctly without infinite looping. if (window.mocha !== undefined && e.target.tagName === 'A') { const href = e.target.getAttribute('href'); if ( href && href.startsWith('/') && !e.target.getAttribute('download') && !e.target.getAttribute('target') ) { e.preventDefault(); e.stopPropagation(); this.$router.push(href); } } }, onDomainAutocompleteChange() { this.isSearchingDomain = false; }, onEditDomainClick() { this.isSearchingDomain = !this.isSearchingDomain; }, onEnvironmentSelectChange(environment) { if (environment === this.environment.value) { return; } window.location = environment.value; }, onNewsDismiss() { localStorage.setItem( LOCAL_STORAGE_NEWS_LAST_VIEWED_AT, this.newsItems[0].date_modified ); }, onNotification({ message, type = NOTIFICATION_TYPE_SUCCESS }) { this.notification.message = message; this.notification.type = type; this.notification.show = true; }, onNotificationClose() { this.notification.show = false; }, onSettingsChange(values) { for (const key in values) { const value = values[key]; const storeValue = typeof value === 'object' ? JSON.stringify(value) : value; localStorage.setItem(LOCAL_STORAGE_SETTINGS[key], storeValue); this.settings[key] = value; } }, onSettingsClick() { this.$modal.show('settings-modal'); }, onWorkflowHistoryEventParamToggle({ eventParam: { key: eventParamName, isHighlighted }, eventType, }) { const { settings: { workflowHistoryEventHighlightList }, } = this; this.settings.workflowHistoryEventHighlightList = workflowHistoryEventHighlightListAddOrUpdate( { eventParamName, eventType, isEnabled: !isHighlighted, workflowHistoryEventHighlightList, } ); localStorage.setItem( LOCAL_STORAGE_SETTINGS.workflowHistoryEventHighlightList, JSON.stringify(this.settings.workflowHistoryEventHighlightList) ); }, }, watch: { 'notification.show'(value) { clearTimeout(this.notification.timeout); if (value) { this.notification.timeout = setTimeout( this.onNotificationClose, NOTIFICATION_TIMEOUT ); } }, }, computed: { version() { return `v${version}`; }, }, }; </script> <template> <main @click="globalClick"> <notification-bar :message="notification.message" :onClose="onNotificationClose" :show="notification.show" :type="notification.type" /> <header class="top-bar"> <flex-grid align-items="center" width="100%"> <flex-grid-item> <a href="/domains" class="logo"> <div v-html="logo"></div> <span class="version">{{ version }}</span> </a> </flex-grid-item> <feature-flag name="environmentSelect"> <flex-grid-item> <select-input class="environment-select" :options="environment.list" :value="environment.value" @change="onEnvironmentSelectChange" /> </flex-grid-item> </feature-flag> <flex-grid-item v-if="$route.params.domain" margin="15px"> <flex-grid align-items="center"> <flex-grid-item> <router-link class="workflows" :to="{ name: 'workflow-list', params: { clusterName: $route.params.clusterName }, }" v-if="!isSearchingDomain" > {{ $route.params.domain }} </router-link> <domain-autocomplete :focus="true" height="slim" v-if="isSearchingDomain" width="500px" @onChange="onDomainAutocompleteChange" /> </flex-grid-item> <flex-grid-item> <button-icon color="primary" :icon="`${isSearchingDomain ? 'icon_delete' : 'icon_search'}`" size="18px" width="22px" @click="onEditDomainClick" /> </flex-grid-item> <flex-grid-item> <active-status :cluster-name="$route.params.clusterName" :domain="$route.params.domain" :workflow-id="$route.params.workflowId" /> </flex-grid-item> </flex-grid> </flex-grid-item> <flex-grid-item v-if="$route.params.workflowId"> <span>{{ $route.params.workflowId }}</span> </flex-grid-item> <flex-grid-item v-if="$route.params.taskList"> <span>{{ $route.params.taskList }}</span> </flex-grid-item> <flex-grid-item grow="1"> <button-icon color="primary" icon="icon_settings" label="SETTINGS" size="30px" style="float: right" @click="onSettingsClick" /> </flex-grid-item> </flex-grid> </header> <cross-region> <domain> <router-view :date-format="settings.dateFormat" :time-format="settings.timeFormat" :timezone="settings.timezone" :workflow-history-event-highlight-list=" settings.workflowHistoryEventHighlightList " :workflow-history-event-highlight-list-enabled=" settings.workflowHistoryEventHighlightListEnabled " @onWorkflowHistoryEventParamToggle="onWorkflowHistoryEventParamToggle" @onNotification="onNotification" ></router-view> </domain> </cross-region> <modals-container /> <v-dialog /> <news-modal :news-items="newsItems" @before-close="onNewsDismiss" /> <settings-modal :date-format="settings.dateFormat" :date-format-options="settings.dateFormatOptions" :time-format="settings.timeFormat" :time-format-options="settings.timeFormatOptions" :timezone="settings.timezone" :timezone-options="settings.timezoneOptions" :workflow-history-event-highlight-list=" settings.workflowHistoryEventHighlightList " :workflow-history-event-highlight-list-enabled=" settings.workflowHistoryEventHighlightListEnabled " @change="onSettingsChange" /> </main> </template> <style src="vue-select/dist/vue-select.css"></style> <style src="vue-virtual-scroller/dist/vue-virtual-scroller.css"></style> <style src="vue2-datepicker/index.css"></style> <style lang="stylus"> @import "https://d1a3f4spazzrp4.cloudfront.net/uber-fonts/4.0.0/superfine.css" @import "https://d1a3f4spazzrp4.cloudfront.net/uber-icons/3.14.0/uber-icons.css" @require "./styles/definitions" @require "./styles/reset" global-reset() @import "./styles/base" @import "./styles/modal" @import "./styles/code" header.top-bar display flex flex 0 0 auto align-items center background-color uber-black padding 0 page-margin-x color base-ui-color height top-nav-height h2 font-size 18px margin-right inline-spacing-large padding page-margin-y inline-spacing-large page-margin-y 0 a display inline-block h2 color uber-white-80 &.config margin-left inline-spacing-medium icon('\ea5f') &.logo { margin-right: layout-spacing-medium; position: relative; } svg display inline-block height top-nav-height - 20px spacing = 1.3em nav-label-color = uber-white-40 nav-label-font-size = 11px .detail-view span::before font-size nav-label-font-size color nav-label-color margin-right spacing div.workflow-id span::before content 'WORKFLOW ID' div.task-list span::before content 'TASK LIST' .version { color: #c6c6c6; font-size: 10px; position: absolute; right: 4px; bottom: 0; } .environment-select { color: #000; .vs__dropdown-toggle { border-color: transparent; } .vs__open-indicator { height: 10px; fill: uber-blue; } .vs__selected { color: white; font-weight: bold; } } html, body { height: 100%; } body, main { display: flex; flex-direction: column; } body { overscroll-behavior: none; } main { flex:1 } main > section display flex flex-direction column flex 1 1 auto > header:last-of-type margin-bottom layout-spacing-small > header display flex align-items: start; flex 0 0 auto > * margin inline-spacing-small area-loader, section.loading size = 32px &::after content '' display block position absolute width size height size border-radius size left "calc(50% - %s)" % (size/2) top 300px; border 3px solid uber-blue border-bottom-color transparent animation spin 800ms linear infinite </style>