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>