Skip to content

Commit 1ecaa07

Browse files
committed
Implement a new header
1 parent 887f0d2 commit 1ecaa07

File tree

13 files changed

+575
-586
lines changed

13 files changed

+575
-586
lines changed

config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module.exports = {
22
catalogUrl: null,
33
catalogTitle: "STAC Browser",
4+
catalogImage: null,
45
allowExternalAccess: true, // Must be true if catalogUrl is not given
56
allowedDomains: [],
67
detectLocaleFromBrowser: true,

docs/options.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The following ways to set config options are possible:
1717

1818
- [catalogUrl](#catalogurl)
1919
- [catalogTitle](#catalogtitle)
20+
- [catalogImage](#catalogimage)
2021
- [allowExternalAccess](#allowexternalaccess)
2122
- [allowedDomains](#alloweddomains)
2223
- [apiCatalogPriority](#apicatalogpriority)
@@ -71,6 +72,11 @@ If `catalogUrl` is empty or set to `null` STAC Browser switches to a mode where
7172

7273
The default title shown if no title can be read from the root STAC catalog.
7374

75+
## catalogImage
76+
77+
URL to an image to use as a logo with the title.
78+
Should be an image that browsers can display, e.g. PNG, JPEG, WebP, or SVG.
79+
7480
## allowExternalAccess
7581

7682
This allows or disallows loading and browsing external STAC data.

src/StacBrowser.vue

Lines changed: 274 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,76 @@
55
<Sidebar v-if="sidebar" />
66
<!-- Header -->
77
<header>
8-
<div class="logo">{{ displayCatalogTitle }}</div>
9-
<StacHeader @enableSidebar="sidebar = true" />
8+
<b-row class="site">
9+
<b-col md="12">
10+
<div class="title">
11+
<img v-if="catalogImageFromVueX" :src="catalogImageFromVueX" class="logo">
12+
<h2>
13+
<StacLink v-if="root" :data="root" />
14+
<template v-else>{{ catalogTitle }}</template>
15+
</h2>
16+
<b-button v-if="root" size="sm" variant="outline-primary" id="popover-root-btn" :title="$t('server')">
17+
<b-icon-server /><span class="button-label">{{ $t('server') }}</span>
18+
</b-button>
19+
</div>
20+
<nav class="actions">
21+
<b-button-group>
22+
<b-button variant="primary" size="sm" :title="$t('browse')" v-b-toggle.sidebar @click="sidebar = true">
23+
<b-icon-list /><span class="button-label">{{ $t('browse') }}</span>
24+
</b-button>
25+
<b-button v-if="canSearch" variant="primary" size="sm" :to="searchBrowserLink" :title="$t('search.title')" :pressed="isSearchPage()">
26+
<b-icon-search /><span class="button-label">{{ $t('search.title') }}</span>
27+
</b-button>
28+
</b-button-group>
29+
</nav>
30+
<nav class="actions">
31+
<b-button-group>
32+
<b-button v-if="canAuthenticate" variant="primary" size="sm" @click="logInOut" :title="authTitle">
33+
<component :is="authIcon" /><span class="button-label">{{ authLabel }}</span>
34+
</b-button>
35+
<b-dropdown size="sm" variant="primary" right :title="$t('source.language.switch')">
36+
<template #button-content>
37+
<b-icon-flag /><span class="button-label">{{ $t('source.language.label', {currentLanguage}) }}</span>
38+
</template>
39+
<b-dropdown-item
40+
v-for="l of languages" :key="l.code" class="lang-item"
41+
@click="switchLocale({locale: l.code, userSelected: true})"
42+
>
43+
<b-icon-check v-if="localeFromVueX === l.code" />
44+
<b-icon-blank v-else />
45+
<span class="title">
46+
<span :lang="l.code">{{ l.native }}</span>
47+
<template v-if="l.global && l.global !== l.native"> / <span lang="en">{{ l.global }}</span></template>
48+
</span>
49+
<b-icon-exclamation-triangle v-if="supportsLanguageExt && (!l.ui || !l.data)" :title="l.ui ? $t('source.language.onlyUI') : $t('source.language.onlyData')" class="ml-2" />
50+
</b-dropdown-item>
51+
</b-dropdown>
52+
</b-button-group>
53+
</nav>
54+
</b-col>
55+
</b-row>
56+
<b-row class="page">
57+
<b-col md="12">
58+
<div class="title">
59+
<img v-if="icon" :src="icon.href" :alt="icon.title" :title="icon.title" class="icon mr-2">
60+
<h1>{{ title }}</h1>
61+
<Source class="title-actions" :title="title" :stacUrl="url" :stac="data" />
62+
</div>
63+
<nav class="actions">
64+
<b-button-group class="actions">
65+
<b-button v-if="back" :to="selfBrowserLink" :title="$t('goBack.description', {type})" variant="outline-primary" size="sm">
66+
<b-icon-arrow-left /><span class="button-label">{{ $t('goBack.label') }}</span>
67+
</b-button>
68+
<b-button v-if="collectionLink" :to="toBrowserPath(collectionLink.href)" :title="collectionLinkTitle" variant="outline-primary" size="sm">
69+
<b-icon-folder-symlink /><span class="button-label">{{ $t('goToCollection.label') }}</span>
70+
</b-button>
71+
<b-button v-if="parentLink" :to="toBrowserPath(parentLink.href)" :title="parentLinkTitle" variant="outline-primary" size="sm">
72+
<b-icon-arrow-90deg-up /><span class="button-label">{{ $t('goToParent.label') }}</span>
73+
</b-button>
74+
</b-button-group>
75+
</nav>
76+
</b-col>
77+
</b-row>
1078
</header>
1179
<!-- Content (Item / Catalog) -->
1280
<router-view />
@@ -17,32 +85,45 @@
1785
</template>
1886
</i18n>
1987
</footer>
88+
<b-popover
89+
v-if="root" id="popover-root" custom-class="popover-large" target="popover-root-btn"
90+
triggers="focus" placement="bottom" container="stac-browser"
91+
>
92+
<template #title>
93+
{{ $t('server') }}
94+
<b-badge v-if="isApi" variant="danger">{{ $t('index.api') }}</b-badge>
95+
<b-badge v-else variant="success">{{ $t('index.catalog') }}</b-badge>
96+
</template>
97+
<RootStats />
98+
</b-popover>
2099
</b-container>
21100
</template>
22101

23102
<script>
24103
import Vue from "vue";
25104
import VueRouter from "vue-router";
26-
import Vuex, { mapActions, mapGetters, mapState } from 'vuex';
105+
import Vuex, { mapMutations, mapActions, mapGetters, mapState } from 'vuex';
27106
import CONFIG from './config';
28107
import getRoutes from "./router";
29108
import getStore from "./store";
30109
31110
import {
32-
AlertPlugin, BadgePlugin, ButtonGroupPlugin, ButtonPlugin,
33-
CardPlugin, LayoutPlugin, SpinnerPlugin,
111+
AlertPlugin, BadgePlugin, BDropdown, BDropdownItem, BPopover,
112+
BIconArrow90degUp, BIconArrowLeft, BIconBlank, BIconCheck, BIconExclamationTriangle,
113+
BIconFlag, BIconFolderSymlink, BIconInfoLg, BIconList, BIconSearch, BIconServer,
114+
ButtonGroupPlugin, ButtonPlugin, CardPlugin, LayoutPlugin, SpinnerPlugin,
34115
VBToggle, VBVisible } from "bootstrap-vue";
35116
import "bootstrap/dist/css/bootstrap.css";
36117
import "bootstrap-vue/dist/bootstrap-vue.css";
37118
38119
import ErrorAlert from './components/ErrorAlert.vue';
39-
import StacHeader from './components/StacHeader.vue';
120+
import StacLink from './components/StacLink.vue';
40121
41122
import STAC from './models/stac';
42123
import Utils from './utils';
43124
import URI from 'urijs';
44125
45-
import { API_LANGUAGE_CONFORMANCE } from './i18n';
126+
import { API_LANGUAGE_CONFORMANCE, STAC_LANGUAGE_EXT } from './i18n';
46127
import { getBest, prepareSupported } from './locale-id';
47128
import BrowserStorage from "./browser-store";
48129
import Authentication from "./components/Authentication.vue";
@@ -103,9 +184,25 @@ export default {
103184
store,
104185
components: {
105186
Authentication,
187+
BDropdown,
188+
BDropdownItem,
189+
BIconArrow90degUp,
190+
BIconArrowLeft,
191+
BIconBlank,
192+
BIconCheck,
193+
BIconExclamationTriangle,
194+
BIconFlag,
195+
BIconFolderSymlink,
196+
BIconInfoLg,
197+
BIconList,
198+
BIconSearch,
199+
BIconServer,
200+
BPopover,
106201
ErrorAlert,
202+
RootStats: () => import('./components/RootStats.vue'),
107203
Sidebar: () => import('./components/Sidebar.vue'),
108-
StacHeader
204+
StacLink,
205+
Source: () => import('./components/Source.vue')
109206
},
110207
props: {
111208
...Props
@@ -118,21 +215,157 @@ export default {
118215
};
119216
},
120217
computed: {
121-
...mapState(['allowSelectCatalog', 'data', 'dataLanguage', 'description', 'globalError', 'stateQueryParameters', 'title', 'uiLanguage', 'url']),
218+
...mapState(['allowSelectCatalog', 'conformsTo', 'data', 'dataLanguage', 'dataLanguages', 'description', 'globalError', 'stateQueryParameters', 'title', 'uiLanguage', 'url']),
122219
...mapState({
220+
catalogImageFromVueX: 'catalogImage',
221+
localeFromVueX: 'locale',
123222
detectLocaleFromBrowserFromVueX: 'detectLocaleFromBrowser',
124223
supportedLocalesFromVueX: 'supportedLocales',
125224
storeLocaleFromVueX: 'storeLocale'
126225
}),
127-
...mapGetters(['displayCatalogTitle', 'fromBrowserPath', 'isExternalUrl', 'root', 'supportsConformance', 'toBrowserPath']),
128-
...mapGetters('auth', ['showLogin']),
226+
...mapGetters(['canSearch', 'collectionLink', 'fromBrowserPath', 'isExternalUrl', 'parentLink', 'root', 'rootLink', 'supportsConformance', 'supportsExtension', 'toBrowserPath']),
227+
...mapGetters('auth', { authMethod: 'method' }),
228+
...mapGetters('auth', ['canAuthenticate', 'isLoggedIn', 'showLogin']),
129229
browserVersion() {
130230
if (typeof STAC_BROWSER_VERSION !== 'undefined') {
131231
return STAC_BROWSER_VERSION;
132232
}
133233
else {
134234
return "";
135235
}
236+
},
237+
authIcon() {
238+
return this.isLoggedIn ? 'b-icon-unlock' : 'b-icon-lock';
239+
},
240+
authTitle() {
241+
return this.authMethod.getButtonTitle();
242+
},
243+
authLabel() {
244+
return this.isLoggedIn ? this.authMethod.getLogoutLabel() : this.authMethod.getLoginLabel();
245+
},
246+
searchBrowserLink() {
247+
if (!this.canSearch) {
248+
return null;
249+
}
250+
let searchLink;
251+
if (this.data instanceof STAC && !this.data.equals(this.root)) {
252+
searchLink = this.data.getSearchLink();
253+
}
254+
if (searchLink) {
255+
return `/search${this.data.getBrowserPath()}`;
256+
}
257+
else if (this.root && this.allowSelectCatalog) {
258+
return `/search${this.root.getBrowserPath()}`;
259+
}
260+
return '/search';
261+
},
262+
currentLanguage() {
263+
let lang = this.languages.find(l => l.code === this.localeFromVueX);
264+
if (lang) {
265+
return lang.native;
266+
}
267+
else {
268+
return '-';
269+
}
270+
},
271+
supportsLanguageExt() {
272+
return this.supportsExtension(STAC_LANGUAGE_EXT);
273+
},
274+
languages() {
275+
let languages = [];
276+
277+
// Add all UI languages
278+
for(let code of this.supportedLocalesFromVueX) {
279+
languages.push({
280+
code,
281+
native: this.$t(`languages.${code}.native`),
282+
global: this.$t(`languages.${code}.global`),
283+
ui: true
284+
});
285+
}
286+
287+
// Add missing data languages
288+
for(let lang of this.dataLanguages) {
289+
if (!Utils.isObject(lang) || !lang.code || this.supportedLocalesFromVueX.includes(lang.code)) {
290+
continue;
291+
}
292+
let newLang = {
293+
code: lang.code
294+
};
295+
newLang.native = lang.name || lang.alternate || lang.code;
296+
newLang.global = lang.alternate || lang.name || lang.code;
297+
newLang.data = true;
298+
languages.push(newLang);
299+
}
300+
301+
if (this.supportsLanguageExt) {
302+
// Determine which languages are complete
303+
const uiSupported = prepareSupported(this.supportedLocalesFromVueX);
304+
const dataSupported = prepareSupported(this.dataLanguages.map(l => l.code));
305+
for(let l of languages) {
306+
if (!l.ui) {
307+
l.ui = Boolean(getBest(uiSupported, l.code, null));
308+
}
309+
if (!l.data) {
310+
l.data = Boolean(getBest(dataSupported, l.code, null));
311+
}
312+
}
313+
}
314+
315+
const collator = new Intl.Collator(this.uiLanguage);
316+
return languages.sort((a,b) => collator.compare(a.global, b.global));
317+
},
318+
isApi() {
319+
// todo: This gives false results for a statically hosted OGC API - Records, which may include conformance classes
320+
return Array.isArray(this.conformsTo) && this.conformsTo.length > 0;
321+
},
322+
back() {
323+
return this.$route.name === 'validation';
324+
},
325+
selfBrowserLink() {
326+
return this.toBrowserPath(this.url);
327+
},
328+
type() {
329+
if (this.data instanceof STAC) {
330+
if (this.data.isItem()) {
331+
return this.$tc('stacItem');
332+
}
333+
else if (this.data.isCollection()) {
334+
return this.$tc(`stacCollection`);
335+
}
336+
else if (this.data.isCatalog()) {
337+
return this.$tc(`stacCatalog`);
338+
}
339+
else if (Utils.hasText(this.data.type)) {
340+
return this.data.type;
341+
}
342+
}
343+
return null;
344+
},
345+
collectionLinkTitle() {
346+
if (this.collectionLink && Utils.hasText(this.collectionLink.title)) {
347+
return this.$t('goToCollection.descriptionWithTitle', this.collectionLink);
348+
}
349+
else {
350+
return this.$t('goToCollection.description');
351+
}
352+
},
353+
parentLinkTitle() {
354+
if (this.parentLink && Utils.hasText(this.parentLink.title)) {
355+
return this.$t('goToParent.descriptionWithTitle', this.parentLink);
356+
}
357+
else {
358+
return this.$t('goToParent.description');
359+
}
360+
},
361+
icon() {
362+
if (this.data instanceof STAC) {
363+
let icons = this.data.getIcons();
364+
if (icons.length > 0) {
365+
return icons[0];
366+
}
367+
}
368+
return null;
136369
}
137370
},
138371
watch: {
@@ -295,6 +528,24 @@ export default {
295528
},
296529
methods: {
297530
...mapActions(['switchLocale']),
531+
...mapMutations('auth', ['addAction']),
532+
...mapActions('auth', ['requestLogin', 'requestLogout']),
533+
async logInOut() {
534+
if (this.url) {
535+
this.addAction(() => this.$store.dispatch("load", {
536+
url: this.url,
537+
show: true,
538+
force: true,
539+
noRetry: true
540+
}));
541+
}
542+
if (this.isLoggedIn) {
543+
await this.requestLogout();
544+
}
545+
else {
546+
await this.requestLogin();
547+
}
548+
},
298549
detectLocale() {
299550
let locale;
300551
if (this.storeLocaleFromVueX) {
@@ -380,6 +631,9 @@ export default {
380631
},
381632
hideError() {
382633
this.$store.commit('showGlobalError', null);
634+
},
635+
isSearchPage() {
636+
return this.$router.currentRoute.name === 'search';
383637
}
384638
}
385639
};
@@ -392,3 +646,12 @@ export default {
392646
@import "./theme/page.scss";
393647
@import "./theme/custom.scss";
394648
</style>
649+
<style lang="scss" scoped>
650+
.lang-item > .dropdown-item {
651+
display: flex;
652+
> .title {
653+
flex: 1;
654+
}
655+
}
656+
</style>
657+

0 commit comments

Comments
 (0)