Skip to content

Implement a new header #601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
catalogUrl: null,
catalogTitle: "STAC Browser",
catalogImage: null,
allowExternalAccess: true, // Must be true if catalogUrl is not given
allowedDomains: [],
detectLocaleFromBrowser: true,
Expand Down
6 changes: 6 additions & 0 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The following ways to set config options are possible:

- [catalogUrl](#catalogurl)
- [catalogTitle](#catalogtitle)
- [catalogImage](#catalogimage)
- [allowExternalAccess](#allowexternalaccess)
- [allowedDomains](#alloweddomains)
- [apiCatalogPriority](#apicatalogpriority)
Expand Down Expand Up @@ -71,6 +72,11 @@ If `catalogUrl` is empty or set to `null` STAC Browser switches to a mode where

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

## catalogImage

URL to an image to use as a logo with the title.
Should be an image that browsers can display, e.g. PNG, JPEG, WebP, or SVG.

## allowExternalAccess

This allows or disallows loading and browsing external STAC data.
Expand Down
285 changes: 274 additions & 11 deletions src/StacBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,76 @@
<Sidebar v-if="sidebar" />
<!-- Header -->
<header>
<div class="logo">{{ displayCatalogTitle }}</div>
<StacHeader @enableSidebar="sidebar = true" />
<b-row class="site">
<b-col md="12">
<div class="title">
<img v-if="catalogImageFromVueX" :src="catalogImageFromVueX" class="logo">
<h2>
<StacLink v-if="root" :data="root" />
<template v-else>{{ catalogTitle }}</template>
</h2>
<b-button v-if="root" size="sm" variant="outline-primary" id="popover-root-btn" :title="$t('server')">
<b-icon-server /><span class="button-label">{{ $t('server') }}</span>
</b-button>
</div>
<nav class="actions">
<b-button-group>
<b-button variant="primary" size="sm" :title="$t('browse')" v-b-toggle.sidebar @click="sidebar = true">
<b-icon-list /><span class="button-label">{{ $t('browse') }}</span>
</b-button>
<b-button v-if="canSearch" variant="primary" size="sm" :to="searchBrowserLink" :title="$t('search.title')" :pressed="isSearchPage()">
<b-icon-search /><span class="button-label">{{ $t('search.title') }}</span>
</b-button>
</b-button-group>
</nav>
<nav class="actions">
<b-button-group>
<b-button v-if="canAuthenticate" variant="primary" size="sm" @click="logInOut" :title="authTitle">
<component :is="authIcon" /><span class="button-label">{{ authLabel }}</span>
</b-button>
<b-dropdown size="sm" variant="primary" right :title="$t('source.language.switch')">
<template #button-content>
<b-icon-flag /><span class="button-label">{{ $t('source.language.label', {currentLanguage}) }}</span>
</template>
<b-dropdown-item
v-for="l of languages" :key="l.code" class="lang-item"
@click="switchLocale({locale: l.code, userSelected: true})"
>
<b-icon-check v-if="localeFromVueX === l.code" />
<b-icon-blank v-else />
<span class="title">
<span :lang="l.code">{{ l.native }}</span>
<template v-if="l.global && l.global !== l.native"> / <span lang="en">{{ l.global }}</span></template>
</span>
<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" />
</b-dropdown-item>
</b-dropdown>
</b-button-group>
</nav>
</b-col>
</b-row>
<b-row class="page">
<b-col md="12">
<div class="title">
<img v-if="icon" :src="icon.href" :alt="icon.title" :title="icon.title" class="icon mr-2">
<h1>{{ title }}</h1>
<Source class="title-actions" :title="title" :stacUrl="url" :stac="data" />
</div>
<nav class="actions">
<b-button-group class="actions">
<b-button v-if="back" :to="selfBrowserLink" :title="$t('goBack.description', {type})" variant="outline-primary" size="sm">
<b-icon-arrow-left /><span class="button-label">{{ $t('goBack.label') }}</span>
</b-button>
<b-button v-if="collectionLink" :to="toBrowserPath(collectionLink.href)" :title="collectionLinkTitle" variant="outline-primary" size="sm">
<b-icon-folder-symlink /><span class="button-label">{{ $t('goToCollection.label') }}</span>
</b-button>
<b-button v-if="parentLink" :to="toBrowserPath(parentLink.href)" :title="parentLinkTitle" variant="outline-primary" size="sm">
<b-icon-arrow-90deg-up /><span class="button-label">{{ $t('goToParent.label') }}</span>
</b-button>
</b-button-group>
</nav>
</b-col>
</b-row>
</header>
<!-- Content (Item / Catalog) -->
<router-view />
Expand All @@ -17,32 +85,45 @@
</template>
</i18n>
</footer>
<b-popover
v-if="root" id="popover-root" custom-class="popover-large" target="popover-root-btn"
triggers="focus" placement="bottom" container="stac-browser"
>
<template #title>
{{ $t('server') }}
<b-badge v-if="isApi" variant="danger">{{ $t('index.api') }}</b-badge>
<b-badge v-else variant="success">{{ $t('index.catalog') }}</b-badge>
</template>
<RootStats />
</b-popover>
</b-container>
</template>

<script>
import Vue from "vue";
import VueRouter from "vue-router";
import Vuex, { mapActions, mapGetters, mapState } from 'vuex';
import Vuex, { mapMutations, mapActions, mapGetters, mapState } from 'vuex';
import CONFIG from './config';
import getRoutes from "./router";
import getStore from "./store";

import {
AlertPlugin, BadgePlugin, ButtonGroupPlugin, ButtonPlugin,
CardPlugin, LayoutPlugin, SpinnerPlugin,
AlertPlugin, BadgePlugin, BDropdown, BDropdownItem, BPopover,
BIconArrow90degUp, BIconArrowLeft, BIconBlank, BIconCheck, BIconExclamationTriangle,
BIconFlag, BIconFolderSymlink, BIconInfoLg, BIconList, BIconSearch, BIconServer,
ButtonGroupPlugin, ButtonPlugin, CardPlugin, LayoutPlugin, SpinnerPlugin,
VBToggle, VBVisible } from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";

import ErrorAlert from './components/ErrorAlert.vue';
import StacHeader from './components/StacHeader.vue';
import StacLink from './components/StacLink.vue';

import STAC from './models/stac';
import Utils from './utils';
import URI from 'urijs';

import { API_LANGUAGE_CONFORMANCE } from './i18n';
import { API_LANGUAGE_CONFORMANCE, STAC_LANGUAGE_EXT } from './i18n';
import { getBest, prepareSupported } from './locale-id';
import BrowserStorage from "./browser-store";
import Authentication from "./components/Authentication.vue";
Expand Down Expand Up @@ -103,9 +184,25 @@ export default {
store,
components: {
Authentication,
BDropdown,
BDropdownItem,
BIconArrow90degUp,
BIconArrowLeft,
BIconBlank,
BIconCheck,
BIconExclamationTriangle,
BIconFlag,
BIconFolderSymlink,
BIconInfoLg,
BIconList,
BIconSearch,
BIconServer,
BPopover,
ErrorAlert,
RootStats: () => import('./components/RootStats.vue'),
Sidebar: () => import('./components/Sidebar.vue'),
StacHeader
StacLink,
Source: () => import('./components/Source.vue')
},
props: {
...Props
Expand All @@ -118,21 +215,157 @@ export default {
};
},
computed: {
...mapState(['allowSelectCatalog', 'data', 'dataLanguage', 'description', 'globalError', 'stateQueryParameters', 'title', 'uiLanguage', 'url']),
...mapState(['allowSelectCatalog', 'conformsTo', 'data', 'dataLanguage', 'dataLanguages', 'description', 'globalError', 'stateQueryParameters', 'title', 'uiLanguage', 'url']),
...mapState({
catalogImageFromVueX: 'catalogImage',
localeFromVueX: 'locale',
detectLocaleFromBrowserFromVueX: 'detectLocaleFromBrowser',
supportedLocalesFromVueX: 'supportedLocales',
storeLocaleFromVueX: 'storeLocale'
}),
...mapGetters(['displayCatalogTitle', 'fromBrowserPath', 'isExternalUrl', 'root', 'supportsConformance', 'toBrowserPath']),
...mapGetters('auth', ['showLogin']),
...mapGetters(['canSearch', 'collectionLink', 'fromBrowserPath', 'isExternalUrl', 'parentLink', 'root', 'rootLink', 'supportsConformance', 'supportsExtension', 'toBrowserPath']),
...mapGetters('auth', { authMethod: 'method' }),
...mapGetters('auth', ['canAuthenticate', 'isLoggedIn', 'showLogin']),
browserVersion() {
if (typeof STAC_BROWSER_VERSION !== 'undefined') {
return STAC_BROWSER_VERSION;
}
else {
return "";
}
},
authIcon() {
return this.isLoggedIn ? 'b-icon-unlock' : 'b-icon-lock';
},
authTitle() {
return this.authMethod.getButtonTitle();
},
authLabel() {
return this.isLoggedIn ? this.authMethod.getLogoutLabel() : this.authMethod.getLoginLabel();
},
searchBrowserLink() {
if (!this.canSearch) {
return null;
}
let searchLink;
if (this.data instanceof STAC && !this.data.equals(this.root)) {
searchLink = this.data.getSearchLink();
}
if (searchLink) {
return `/search${this.data.getBrowserPath()}`;
}
else if (this.root && this.allowSelectCatalog) {
return `/search${this.root.getBrowserPath()}`;
}
return '/search';
},
currentLanguage() {
let lang = this.languages.find(l => l.code === this.localeFromVueX);
if (lang) {
return lang.native;
}
else {
return '-';
}
},
supportsLanguageExt() {
return this.supportsExtension(STAC_LANGUAGE_EXT);
},
languages() {
let languages = [];

// Add all UI languages
for(let code of this.supportedLocalesFromVueX) {
languages.push({
code,
native: this.$t(`languages.${code}.native`),
global: this.$t(`languages.${code}.global`),
ui: true
});
}

// Add missing data languages
for(let lang of this.dataLanguages) {
if (!Utils.isObject(lang) || !lang.code || this.supportedLocalesFromVueX.includes(lang.code)) {
continue;
}
let newLang = {
code: lang.code
};
newLang.native = lang.name || lang.alternate || lang.code;
newLang.global = lang.alternate || lang.name || lang.code;
newLang.data = true;
languages.push(newLang);
}

if (this.supportsLanguageExt) {
// Determine which languages are complete
const uiSupported = prepareSupported(this.supportedLocalesFromVueX);
const dataSupported = prepareSupported(this.dataLanguages.map(l => l.code));
for(let l of languages) {
if (!l.ui) {
l.ui = Boolean(getBest(uiSupported, l.code, null));
}
if (!l.data) {
l.data = Boolean(getBest(dataSupported, l.code, null));
}
}
}

const collator = new Intl.Collator(this.uiLanguage);
return languages.sort((a,b) => collator.compare(a.global, b.global));
},
isApi() {
// todo: This gives false results for a statically hosted OGC API - Records, which may include conformance classes
return Array.isArray(this.conformsTo) && this.conformsTo.length > 0;
},
back() {
return this.$route.name === 'validation';
},
selfBrowserLink() {
return this.toBrowserPath(this.url);
},
type() {
if (this.data instanceof STAC) {
if (this.data.isItem()) {
return this.$tc('stacItem');
}
else if (this.data.isCollection()) {
return this.$tc(`stacCollection`);
}
else if (this.data.isCatalog()) {
return this.$tc(`stacCatalog`);
}
else if (Utils.hasText(this.data.type)) {
return this.data.type;
}
}
return null;
},
collectionLinkTitle() {
if (this.collectionLink && Utils.hasText(this.collectionLink.title)) {
return this.$t('goToCollection.descriptionWithTitle', this.collectionLink);
}
else {
return this.$t('goToCollection.description');
}
},
parentLinkTitle() {
if (this.parentLink && Utils.hasText(this.parentLink.title)) {
return this.$t('goToParent.descriptionWithTitle', this.parentLink);
}
else {
return this.$t('goToParent.description');
}
},
icon() {
if (this.data instanceof STAC) {
let icons = this.data.getIcons();
if (icons.length > 0) {
return icons[0];
}
}
return null;
}
},
watch: {
Expand Down Expand Up @@ -295,6 +528,24 @@ export default {
},
methods: {
...mapActions(['switchLocale']),
...mapMutations('auth', ['addAction']),
...mapActions('auth', ['requestLogin', 'requestLogout']),
async logInOut() {
if (this.url) {
this.addAction(() => this.$store.dispatch("load", {
url: this.url,
show: true,
force: true,
noRetry: true
}));
}
if (this.isLoggedIn) {
await this.requestLogout();
}
else {
await this.requestLogin();
}
},
detectLocale() {
let locale;
if (this.storeLocaleFromVueX) {
Expand Down Expand Up @@ -380,6 +631,9 @@ export default {
},
hideError() {
this.$store.commit('showGlobalError', null);
},
isSearchPage() {
return this.$router.currentRoute.name === 'search';
}
}
};
Expand All @@ -392,3 +646,12 @@ export default {
@import "./theme/page.scss";
@import "./theme/custom.scss";
</style>
<style lang="scss" scoped>
.lang-item > .dropdown-item {
display: flex;
> .title {
flex: 1;
}
}
</style>

Loading