Initial commit
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<Page class="page--settings">
|
||||
<div class="connectedapps grid">
|
||||
<SettingsSidebar class="grid-cell--col4" :userModel="user" :baseUrl="baseUrl" />
|
||||
<div class="conectedapps__container grid-cell--col8">
|
||||
<div class="connectedapps__title">
|
||||
<h2 class="text is-caption">{{ $t(`ConnectedAppsPage.title`) }}</h2>
|
||||
</div>
|
||||
|
||||
<p v-if="hasConnectedApps" class="text is-small">{{ $t(`ConnectedAppsPage.description`) }}</p>
|
||||
<p v-else class="text is-small u-mb--96">{{ $t(`ConnectedAppsPage.emptyDescription`) }}</p>
|
||||
|
||||
<div v-if="!hasConnectedApps" class="u-mt--16">
|
||||
<div class="connectedapps__title">
|
||||
<h2 class="text is-caption">{{ $t(`ConnectedAppsPage.emptyTipTitle`) }}</h2>
|
||||
</div>
|
||||
<p class="text is-small"
|
||||
v-html="$t('ConnectedAppsPage.emptyTipDescription', { url: this.$router.resolve({ name: 'oauth_apps_list' }).href })">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="hasConnectedApps" class="connectedapps__list">
|
||||
<div class="connectedapps__list-title">
|
||||
<h3 class="text is-small is-semibold">{{ $t(`ConnectedAppsPage.listTitle`) }}</h3>
|
||||
</div>
|
||||
<ul>
|
||||
<AppElement v-for="connectedApp in connectedApps" :key="connectedApp.id" :oAuthApp="connectedApp">
|
||||
<button class="connectedapps__button connectedapps__button--revoke button button--ghost" @click="openModal(connectedApp)">
|
||||
{{ $t(`ConnectedAppsPage.removeAccessButton`) }}
|
||||
</button>
|
||||
</AppElement>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal :isOpen="isModalOpen" @closeModal="closeModal">
|
||||
<div class="connectedapps__modal">
|
||||
<div class="connectedapps__modal-inner">
|
||||
<div class="connectedapps__icon u-mb--24">
|
||||
<img svg-inline src="../../assets/icons/apps/default.svg">
|
||||
<img class="connectedapps__badge" svg-inline src="../../assets/icons/apps/trash.svg">
|
||||
</div>
|
||||
<span class="text is-caption u-mb--8" v-html="$t(`ConnectedAppsPage.removeModal.title`, { name: selectedApp.name })"></span>
|
||||
<span class="text is-small is-txtSoftGrey">{{ $t(`ConnectedAppsPage.removeModal.subtitle`) }}</span>
|
||||
<div class="connectedapps__modal-actions">
|
||||
<button class="connectedapps__button button button--ghost is-primary u-mr--12" @click="closeModal">{{ $t(`ConnectedAppsPage.removeModal.cancelButton`) }}</button>
|
||||
<button class="connectedapps__button button button--alert text is-small" @click="revokeAccess(selectedApp)">{{ $t(`ConnectedAppsPage.removeModal.removeButton`) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Page from 'new-dashboard/components/Page';
|
||||
import AppElement from '../../components/Apps/AppElement';
|
||||
import SettingsSidebar from 'new-dashboard/components/Apps/SettingsSidebar';
|
||||
import Modal from 'new-dashboard/components/Modal';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ConnectedApps',
|
||||
components: {
|
||||
AppElement,
|
||||
Page,
|
||||
SettingsSidebar,
|
||||
Modal
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isModalOpen: false,
|
||||
selectedApp: {}
|
||||
};
|
||||
},
|
||||
beforeMount: function () {
|
||||
this.$store.dispatch('connectedApps/fetch', {
|
||||
apiKey: this.$store.state.user.api_key
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isFetchingConnectedApps: state => state.connectedApps.isFetching,
|
||||
user: state => state.user,
|
||||
baseUrl: state => state.user.base_url,
|
||||
connectedApps: state => state.connectedApps.list,
|
||||
hasConnectedApps: state => !state.connectedApps.isFetching && !!Object.keys(state.connectedApps.list).length
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
openModal (selectedApp) {
|
||||
this.isModalOpen = true;
|
||||
this.selectedApp = selectedApp;
|
||||
},
|
||||
closeModal () {
|
||||
this.isModalOpen = false;
|
||||
this.selectedApp = {};
|
||||
},
|
||||
revokeAccess (selectedApp) {
|
||||
this.$store
|
||||
.dispatch('connectedApps/revoke', selectedApp)
|
||||
.then(() => {
|
||||
this.closeModal();
|
||||
this.$router.push({ name: 'connected_apps' });
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.connectedapps {
|
||||
display: flex;
|
||||
width: 940px;
|
||||
min-height: 585px;
|
||||
margin: 48px auto 0;
|
||||
padding: 0;
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
width: 100;
|
||||
padding-left: 82px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 28px;
|
||||
border-bottom: 1px solid $neutral--300;
|
||||
}
|
||||
|
||||
&__list {
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
&__list-title {
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid $neutral--300;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid $neutral--300;
|
||||
}
|
||||
|
||||
&__item-info {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__item-title {
|
||||
max-width: 356px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
&__item-description {
|
||||
max-width: 356px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 1px solid $neutral--300;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
right: -9px;
|
||||
animation: fade-and-bounce-up 0.6s 0.35s ease-in-out backwards;
|
||||
}
|
||||
}
|
||||
|
||||
.connectedapps__button {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
|
||||
&.connectedapps__button--revoke {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.button--ghost {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid $blue--500;
|
||||
background: none;
|
||||
color: $blue--500;
|
||||
}
|
||||
|
||||
.button--uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.connectedapps__modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.connectedapps__modal-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.connectedapps__modal-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-top: 54px;
|
||||
padding-top: 38px;
|
||||
border-top: 1px solid $neutral--300;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!-- Base Component for OAuth pages -->
|
||||
<template>
|
||||
<Page class="page--settings">
|
||||
<div class="oauthapps grid">
|
||||
<SettingsSidebar class="grid-cell--col4" :userModel="user" :baseUrl="baseUrl" />
|
||||
|
||||
<!-- Form -->
|
||||
<div class="oauthapps__container grid-cell--col8">
|
||||
<AppTabs></AppTabs>
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Page from 'new-dashboard/components/Page';
|
||||
import SettingsSidebar from 'new-dashboard/components/Apps/SettingsSidebar';
|
||||
import AppTabs from 'new-dashboard/components/Apps/AppTabs';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'OAuthApps',
|
||||
components: {
|
||||
Page,
|
||||
SettingsSidebar,
|
||||
AppTabs
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: state => state.user,
|
||||
baseUrl: state => state.user.base_url
|
||||
})
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.oauthapps {
|
||||
display: flex;
|
||||
width: 940px;
|
||||
min-height: 585px;
|
||||
margin: 48px auto 0;
|
||||
padding: 0;
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding-left: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<section class="catalog">
|
||||
<div class="container grid">
|
||||
<div class="full-width">
|
||||
<SectionTitle class="grid-cell">
|
||||
<template slot="icon">
|
||||
<img src="../../assets/icons/section-title/catalog.svg" width="18" height="20" />
|
||||
</template>
|
||||
<template slot="title">
|
||||
<span>{{ $t('CatalogPage.header') }}</span>
|
||||
</template>
|
||||
</SectionTitle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="catalog__filter container grid" action="">
|
||||
<div class="grid-cell grid-cell--col6 catalog__filter--dropdown">
|
||||
<CatalogDropdown ref="dropdownCategories"
|
||||
:title="$t('CatalogDropdown.category.title')"
|
||||
:placeholder="$t('CatalogDropdown.category.placeholder')"
|
||||
:options="categories"
|
||||
:disabled="true"
|
||||
@selected="getCountries"
|
||||
@reset="resetCategory">
|
||||
<template slot="extra">
|
||||
<span v-html="$t('CatalogDropdown.category.extra')" class="text is-small"></span>
|
||||
</template>
|
||||
</CatalogDropdown>
|
||||
</div>
|
||||
<div class="grid-cell grid-cell--col6 catalog__filter--dropdown">
|
||||
<CatalogDropdown ref="dropdownCountries"
|
||||
:title="$t('CatalogDropdown.country.title')"
|
||||
:placeholder="$t('CatalogDropdown.country.placeholder')"
|
||||
:options="countries"
|
||||
:disabled="true"
|
||||
:limitHeight="true"
|
||||
@open="false"
|
||||
@selected="getDatasets"
|
||||
@reset="resetCountry">
|
||||
<template slot="extra">
|
||||
<span v-html="$t('CatalogDropdown.country.extra')" class="text is-small"></span>
|
||||
</template>
|
||||
</CatalogDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datasets list -->
|
||||
<div class="container grid" v-if="datasets.length > 0">
|
||||
<div class="grid-cell grid-cell--col12">
|
||||
<CatalogListHeader :order="appliedOrder" :orderDirection="appliedOrderDirection" @changeOrder="applyOrder"></CatalogListHeader>
|
||||
<ul v-if="!isFetchingDatasets" >
|
||||
<li v-for="(dataset) in currentPageDatasets" :key="dataset.id">
|
||||
<CatalogCard :dataset="dataset"></CatalogCard>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul v-else>
|
||||
<li v-for="(n) in resultsPerPage" :key="n">
|
||||
<CatalogFakeCard />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination v-if="shouldShowPagination" :page=currentPage :numPages=numPages @pageChange="goToPage"></Pagination>
|
||||
</section>
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import sendCustomEvent from 'new-dashboard/utils/send-custom-event';
|
||||
import SectionTitle from 'new-dashboard/components/SectionTitle';
|
||||
import CatalogListHeader from 'new-dashboard/components/Catalog/CatalogListHeader';
|
||||
import CatalogCard from 'new-dashboard/components/Catalog/CatalogCard';
|
||||
import CatalogFakeCard from 'new-dashboard/components/Catalog/CatalogFakeCard';
|
||||
import CatalogDropdown from 'new-dashboard/components/Catalog/CatalogDropdown';
|
||||
import Pagination from 'new-dashboard/components/Pagination';
|
||||
|
||||
export default {
|
||||
name: 'CatalogPage',
|
||||
components: {
|
||||
SectionTitle,
|
||||
CatalogListHeader,
|
||||
CatalogCard,
|
||||
CatalogFakeCard,
|
||||
CatalogDropdown,
|
||||
Pagination
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
sendCustomEvent('catalogSelectCategory', {
|
||||
catalogSelectedCategory: to.query.category
|
||||
});
|
||||
sendCustomEvent('catalogSelectCountry', {
|
||||
catalogSelectedCountry: to.query.country
|
||||
});
|
||||
next();
|
||||
},
|
||||
mounted () {
|
||||
const category = this.$route.query.category;
|
||||
const country = this.$route.query.country;
|
||||
if (category && country) {
|
||||
this.setUrlOptions();
|
||||
} else {
|
||||
this.getCategories();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
numPages: state => state.catalog.numPages,
|
||||
currentPage: state => state.catalog.page,
|
||||
datasets: state => state.catalog.list,
|
||||
categories: state => state.catalog.categories,
|
||||
countries: state => state.catalog.countries,
|
||||
isFetchingDatasets: state => state.catalog.isFetching,
|
||||
numResults: state => state.catalog.umResults,
|
||||
resultsPerPage: state => state.catalog.resultsPerPage,
|
||||
appliedOrder: state => state.catalog.order,
|
||||
appliedOrderDirection: state => state.catalog.orderDirection
|
||||
}),
|
||||
shouldShowPagination () {
|
||||
return !this.isFetchingDatasets && this.numPages > 1;
|
||||
},
|
||||
currentPageDatasets () {
|
||||
return this.datasets.slice(((this.currentPage - 1) * this.resultsPerPage), (this.currentPage * this.resultsPerPage));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToPage (page) {
|
||||
window.scroll({ top: 0, left: 0 });
|
||||
this.$router.push({
|
||||
name: 'catalog',
|
||||
params: this.$route.params,
|
||||
query: { ...this.$route.query, page }
|
||||
});
|
||||
},
|
||||
applyOrder (orderParams) {
|
||||
this.$router.push({
|
||||
name: 'catalog',
|
||||
params: this.$route.params,
|
||||
query: {
|
||||
...this.$route.query,
|
||||
page: 1,
|
||||
order: orderParams.order,
|
||||
order_direction: orderParams.direction
|
||||
}
|
||||
});
|
||||
this.$store.dispatch('catalog/order', orderParams);
|
||||
},
|
||||
getCategories () {
|
||||
this.clearList();
|
||||
this.$store.dispatch('catalog/fetchCategories')
|
||||
.then(
|
||||
() => {
|
||||
this.$refs.dropdownCategories.enableDropdown();
|
||||
this.$refs.dropdownCategories.openDropdown();
|
||||
},
|
||||
(err) => {
|
||||
this.$refs.dropdownCategories.enableDropdown();
|
||||
this.$refs.dropdownCategories.setError(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
getCountries (category) {
|
||||
this.clearList();
|
||||
this.$store.dispatch('catalog/fetchCountries', category)
|
||||
.then(
|
||||
() => {
|
||||
this.$refs.dropdownCountries.clearInput();
|
||||
this.$refs.dropdownCountries.enableDropdown();
|
||||
window.scroll({ top: 0, left: 0 });
|
||||
},
|
||||
(err) => {
|
||||
this.$refs.dropdownCountries.enableDropdown();
|
||||
this.$refs.dropdownCountries.setError(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
getDatasets (country) {
|
||||
this.$store.dispatch('catalog/fetchDatasets', {
|
||||
category: this.$refs.dropdownCategories.selected,
|
||||
country
|
||||
}).then(
|
||||
() => {
|
||||
this.$router.push({
|
||||
name: 'catalog',
|
||||
query: {
|
||||
category: this.$refs.dropdownCategories.selected,
|
||||
country
|
||||
}
|
||||
});
|
||||
window.scroll({ top: 0, left: 0 });
|
||||
},
|
||||
(err) => {
|
||||
this.$refs.dropdownCategories.setError(err);
|
||||
this.$refs.dropdownCountries.setError(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
resetCategory () {
|
||||
this.$refs.dropdownCountries.clearInput();
|
||||
this.$refs.dropdownCountries.disableDropdown();
|
||||
this.clearList();
|
||||
},
|
||||
resetCountry () {
|
||||
this.clearList();
|
||||
},
|
||||
clearList () {
|
||||
this.$store.dispatch('catalog/clearList');
|
||||
},
|
||||
setUrlOptions () {
|
||||
const category = this.$route.query.category;
|
||||
const country = this.$route.query.country;
|
||||
const promiseDatasets = this.$store.dispatch('catalog/fetchDatasets', {
|
||||
category: category,
|
||||
country: country
|
||||
});
|
||||
const promiseCategories = this.$store.dispatch('catalog/fetchCategories');
|
||||
const promiseCountires = this.$store.dispatch('catalog/fetchCountries', category);
|
||||
Promise.all([promiseDatasets, promiseCategories, promiseCountires])
|
||||
.then(
|
||||
() => {
|
||||
if (this.datasets.length > 0) {
|
||||
this.$refs.dropdownCategories.setInput(category);
|
||||
this.$refs.dropdownCountries.setInput(country);
|
||||
} else {
|
||||
this.getCategories();
|
||||
this.$router.push({ name: 'catalog' });
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.$router.push({ name: 'catalog' });
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.catalog {
|
||||
min-height: 640px;
|
||||
margin-top: 64px;
|
||||
|
||||
&__filter {
|
||||
justify-content: space-between;
|
||||
height: 168px;
|
||||
|
||||
&--dropdown {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<Page class="page--data">
|
||||
<SecondaryNavigation>
|
||||
<a class="catalogDetail__back title is-small" href="javascript:history.back()">
|
||||
<img class="catalogDetail__back--icon" svg-inline src="../../assets/icons/common/back.svg"/>
|
||||
<span>{{ $t('CatalogDetailPage.back') }}</span>
|
||||
</a>
|
||||
</SecondaryNavigation>
|
||||
|
||||
<section v-if="!isFetchingDatasets" class="catalogDetail">
|
||||
<div class="container grid">
|
||||
<div class="full-width">
|
||||
<SectionTitle class="grid-cell">
|
||||
<template slot="icon">
|
||||
<img src="../../assets/icons/section-title/data.svg" width="18" height="20" />
|
||||
</template>
|
||||
<template slot="title">
|
||||
<span>{{ dataset.name }}</span>
|
||||
</template>
|
||||
|
||||
<template slot="actionButton">
|
||||
<CatalogRequestSuccess v-if="hasBeenSuccesfullyRequested"></CatalogRequestSuccess>
|
||||
<button v-else class="button is-primary" @click="requestDataset">{{ $t('CatalogDetailPage.requestDataset') }}</button>
|
||||
</template>
|
||||
</SectionTitle>
|
||||
|
||||
<div class="catalogDetail__description grid-cell">
|
||||
<span class="text is-caption">
|
||||
{{ dataset.description }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-cell grid-cell--col6">
|
||||
<h4 class="catalogDetail__label title is-caption">{{ $t('CatalogDetailPage.variableGroups') }}</h4>
|
||||
<ul v-for="variable in dataset.variable_name" :key="variable" class="catalogDetail__list">
|
||||
<li class="text is-caption catalogDetail__list--item">{{variable}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="grid-cell grid-cell--col3">
|
||||
<h4 class="catalogDetail__label title is-caption">{{ $t('CatalogDetailPage.category') }}</h4>
|
||||
<div class="u-flex">
|
||||
<div :class="`catalog__icon catalog__icon--${getCSSModifier(dataset.category)}`"></div>
|
||||
<p class="text is-caption">{{ dataset.category }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-cell grid-cell--col3">
|
||||
<h4 class="catalogDetail__label title is-caption">{{ $t('CatalogDetailPage.country') }}</h4>
|
||||
<div class="u-flex">
|
||||
<div :class="`catalog__icon catalog__icon--${getCSSModifier(dataset.country)}`"></div>
|
||||
<p class="text is-caption">{{ dataset.country }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import toObject from 'new-dashboard/utils/to-object';
|
||||
import getCSSModifier from 'new-dashboard/utils/get-css-modifier';
|
||||
import sendCustomEvent from 'new-dashboard/utils/send-custom-event';
|
||||
import Page from 'new-dashboard/components/Page';
|
||||
import SecondaryNavigation from 'new-dashboard/components/SecondaryNavigation';
|
||||
import SectionTitle from 'new-dashboard/components/SectionTitle';
|
||||
import CatalogRequestSuccess from 'new-dashboard/components/Catalog/CatalogRequestSuccess';
|
||||
|
||||
export default {
|
||||
name: 'CatalogDetail',
|
||||
components: {
|
||||
Page,
|
||||
SecondaryNavigation,
|
||||
SectionTitle,
|
||||
CatalogRequestSuccess
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
hasBeenSuccesfullyRequested: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: state => state.user,
|
||||
datasets: state => state.catalog.list,
|
||||
isFetchingDatasets: state => state.catalog.isFetching,
|
||||
dataset (state) {
|
||||
if (this.isFetchingDatasets) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const datasetList = toObject(this.datasets, 'id');
|
||||
const selectedDataset = datasetList[this.$route.params.id];
|
||||
sendCustomEvent('catalogSelectDataset', {
|
||||
catalogSelectedDataset: selectedDataset.name
|
||||
});
|
||||
return selectedDataset;
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
getCSSModifier,
|
||||
requestDataset () {
|
||||
this.$store.dispatch('catalog/requestDataset', { user: this.user, dataset: this.dataset })
|
||||
.then(
|
||||
() => {
|
||||
this.hasBeenSuccesfullyRequested = true;
|
||||
this.sendCustomCatalogEvents(this.dataset);
|
||||
}
|
||||
);
|
||||
},
|
||||
sendCustomCatalogEvents (dataset) {
|
||||
sendCustomEvent('catalogRequestDataset', {
|
||||
catalogRequestedDataset: dataset.name
|
||||
});
|
||||
sendCustomEvent('catalogRequestCountry', {
|
||||
catalogRequestedCountry: dataset.country
|
||||
});
|
||||
sendCustomEvent('catalogRequestCategory', {
|
||||
catalogRequestedCategory: dataset.category
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "new-dashboard/styles/variables";
|
||||
|
||||
.page--data {
|
||||
padding-top: 64px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.catalogDetail {
|
||||
margin-top: 64px;
|
||||
|
||||
&__back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24px 0;
|
||||
|
||||
&--icon {
|
||||
width: 7px;
|
||||
height: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
list-style-position: inside;
|
||||
list-style-type: disc;
|
||||
|
||||
&--item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.catalog__icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
background-size: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<Page class="page--data">
|
||||
<SecondaryNavigation>
|
||||
<div class="tabs">
|
||||
<router-link :to="{ name: 'datasets' }" class="tabs__item title is-small" exact active-class="is-active" :class="{'is-active': isDatasetPage }">
|
||||
<span>{{ $t('DataPage.tabs.yourDatasets') }}</span>
|
||||
</router-link>
|
||||
<router-link :to="{ name: 'catalog' }" class="tabs__item title is-small" active-class="is-active">
|
||||
<span>{{ $t('DataPage.tabs.catalog') }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</SecondaryNavigation>
|
||||
<router-view></router-view>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Page from 'new-dashboard/components/Page';
|
||||
import SecondaryNavigation from 'new-dashboard/components/SecondaryNavigation';
|
||||
import { isAllowed } from 'new-dashboard/core/configuration/filters';
|
||||
|
||||
export default {
|
||||
name: 'DataPage',
|
||||
components: {
|
||||
Page,
|
||||
SecondaryNavigation
|
||||
},
|
||||
computed: {
|
||||
isDatasetPage () {
|
||||
return isAllowed(this.$route.params.filter);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "new-dashboard/styles/variables";
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
|
||||
&__item {
|
||||
margin-right: 48px;
|
||||
padding: 24px 0 20px;
|
||||
border-bottom: 4px solid transparent;
|
||||
background: none;
|
||||
color: $color-primary;
|
||||
text-transform: none;
|
||||
|
||||
&.is-active {
|
||||
border-color: currentColor;
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<section class="datasets-section">
|
||||
<StickySubheader :is-visible="Boolean(selectedDatasets.length && isScrollPastHeader)">
|
||||
<h2 class="title is-caption">
|
||||
{{ $t('BulkActions.selected', {count: selectedDatasets.length}) }}
|
||||
</h2>
|
||||
<DatasetBulkActions
|
||||
:selectedDatasets="selectedDatasets"
|
||||
:areAllDatasetsSelected="areAllDatasetsSelected"
|
||||
@selectAll="selectAll"
|
||||
@deselectAll="deselectAll"></DatasetBulkActions>
|
||||
</StickySubheader>
|
||||
|
||||
<DatasetsList
|
||||
ref="datasetsList"
|
||||
class="grid__content"
|
||||
:hasBulkActions="true"
|
||||
:canHoverCard="true"
|
||||
:maxVisibleDatasets="maxVisibleDatasets"
|
||||
@applyFilter="applyFilter"
|
||||
@applyOrder="applyOrder"
|
||||
@selectionChange="updateSelected" />
|
||||
<Pagination v-if="shouldShowPagination" :page=currentPage :numPages=numPages @pageChange="goToPage"></Pagination>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { mapState } from 'vuex';
|
||||
import { checkFilters } from 'new-dashboard/router/hooks/check-navigation';
|
||||
import Pagination from 'new-dashboard/components/Pagination';
|
||||
import DatasetBulkActions from 'new-dashboard/components/BulkActions/DatasetBulkActions.vue';
|
||||
import StickySubheader from '../../components/StickySubheader';
|
||||
import DatasetsList from 'new-dashboard/components/DatasetsList.vue';
|
||||
|
||||
export default {
|
||||
name: 'DatasetsPage',
|
||||
components: {
|
||||
DatasetBulkActions,
|
||||
StickySubheader,
|
||||
Pagination,
|
||||
DatasetsList
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isScrollPastHeader: false,
|
||||
maxVisibleDatasets: 12,
|
||||
selectedDatasets: []
|
||||
};
|
||||
},
|
||||
beforeMount () {
|
||||
if (this.$store.getters['user/isViewer']) {
|
||||
// Redirect to shared datasets page if user is viewer
|
||||
return this.$router.replace({ name: 'datasets', params: { filter: 'shared' } });
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.stickyScrollPosition = this.getHeaderBottomPageOffset();
|
||||
this.$onScrollChange = this.onScrollChange.bind(this);
|
||||
document.addEventListener('scroll', this.$onScrollChange, { passive: true });
|
||||
},
|
||||
beforeDestroy () {
|
||||
document.removeEventListener('scroll', this.$onScrollChange, { passive: true });
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
checkFilters('datasets', 'datasets', to, from, next);
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
numPages: state => state.datasets.numPages,
|
||||
currentPage: state => state.datasets.page,
|
||||
datasets: state => state.datasets.list,
|
||||
isFetchingDatasets: state => state.datasets.isFetching,
|
||||
numResults: state => state.datasets.metadata.total_entries
|
||||
}),
|
||||
isNotificationVisible () {
|
||||
return this.$store.getters['user/isNotificationVisible'];
|
||||
},
|
||||
areAllDatasetsSelected () {
|
||||
return Object.keys(this.datasets).length === this.selectedDatasets.length;
|
||||
},
|
||||
shouldShowPagination () {
|
||||
return !this.isFetchingDatasets && this.numResults > 0 && this.numPages > 1;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateSelected (selected) {
|
||||
this.selectedDatasets = selected;
|
||||
},
|
||||
goToPage (page) {
|
||||
this.deselectAll();
|
||||
window.scroll({ top: 0, left: 0 });
|
||||
|
||||
this.$router.push({
|
||||
name: 'datasets',
|
||||
params: this.$route.params,
|
||||
query: {...this.$route.query, page}
|
||||
});
|
||||
},
|
||||
applyFilter (filter) {
|
||||
this.$router.push({ name: 'datasets', params: { filter } });
|
||||
},
|
||||
applyOrder (orderParams) {
|
||||
this.deselectAll();
|
||||
this.$router.push({
|
||||
name: 'datasets',
|
||||
params: this.$route.params,
|
||||
query: {
|
||||
...this.$route.query,
|
||||
page: 1,
|
||||
order: orderParams.order,
|
||||
order_direction: orderParams.direction
|
||||
}
|
||||
});
|
||||
},
|
||||
selectAll () {
|
||||
this.$refs.datasetsList.selectAll();
|
||||
},
|
||||
deselectAll () {
|
||||
this.$refs.datasetsList.deselectAll();
|
||||
},
|
||||
onScrollChange () {
|
||||
this.isScrollPastHeader = window.pageYOffset > this.stickyScrollPosition;
|
||||
},
|
||||
getHeaderBottomPageOffset () {
|
||||
const headerContainer = this.$refs.datasetsList.getHeaderContainer();
|
||||
const headerBoundingClientRect = headerContainer.$el.getBoundingClientRect();
|
||||
const notificationHeight = this.isNotificationVisible ? 60 : 0;
|
||||
return headerBoundingClientRect.top - notificationHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.datasets-section {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<section>
|
||||
<DatasetsList
|
||||
:hasBulkActions="false"
|
||||
:canHoverCard="false"
|
||||
:maxVisibleDatasets="maxVisibleDatasets"
|
||||
@applyFilter="applyFilter"
|
||||
@applyOrder="applyOrder"
|
||||
@contentChanged="onContentChanged"/>
|
||||
|
||||
<router-link :to="{ name: 'datasets' }" class="title is-small viewall-link" v-if="showViewAllLink">
|
||||
{{ datasetsLinkText }}
|
||||
</router-link>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import DatasetsList from 'new-dashboard/components/DatasetsList.vue';
|
||||
|
||||
export default {
|
||||
name: 'DatasetsSection',
|
||||
components: {
|
||||
DatasetsList
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
maxVisibleDatasets: 6
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isFetchingDatasets: state => state.datasets.isFetching,
|
||||
currentEntriesCount: state => state.datasets.metadata.total_entries
|
||||
}),
|
||||
datasetsLinkText () {
|
||||
return this.$t('HomePage.DatasetsSection.viewAll');
|
||||
},
|
||||
showViewAllLink () {
|
||||
return !this.isFetchingDatasets && this.currentEntriesCount;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
applyOrder (orderOptions) {
|
||||
this.$store.dispatch('datasets/order', orderOptions);
|
||||
this.$store.dispatch('datasets/fetch');
|
||||
},
|
||||
applyFilter (filter) {
|
||||
this.$store.dispatch('datasets/filter', filter);
|
||||
this.$store.dispatch('datasets/fetch');
|
||||
},
|
||||
fetchDatasets () {
|
||||
this.$store.dispatch('datasets/fetch');
|
||||
},
|
||||
onContentChanged (type) {
|
||||
this.$emit('contentChanged', type);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "new-dashboard/styles/variables";
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.viewall-link {
|
||||
display: block;
|
||||
margin-top: 64px;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<Page class="page--welcome">
|
||||
<Welcome />
|
||||
<RecentSection class="section" v-if="isSectionActive('RecentSection') && hasRecentContent" @sectionChange="changeSection" @contentChanged="onContentChanged"/>
|
||||
<TagsSection class="section tags-section" v-if="isSectionActive('TagsSection')" @sectionChange="changeSection"/>
|
||||
<MapsSection class="section" @contentChanged="onContentChanged"/>
|
||||
<DatasetsSection class="section section--noBorder" @contentChanged="onContentChanged"/>
|
||||
<QuotaSection></QuotaSection>
|
||||
|
||||
<router-view name="onboarding-modal"/>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Welcome from './WelcomeSection/Welcome.vue';
|
||||
import TagsSection from './TagsSection/TagsSection.vue';
|
||||
import RecentSection from './RecentSection/RecentSection.vue';
|
||||
import MapsSection from './MapsSection/MapsSection.vue';
|
||||
import DatasetsSection from './DatasetsSection/DatasetsSection.vue';
|
||||
import QuotaSection from './QuotaSection/QuotaSection.vue';
|
||||
import Page from 'new-dashboard/components/Page';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
Welcome,
|
||||
TagsSection,
|
||||
RecentSection,
|
||||
MapsSection,
|
||||
DatasetsSection,
|
||||
QuotaSection,
|
||||
Page
|
||||
},
|
||||
beforeMount () {
|
||||
this.$store.dispatch('recentContent/fetch');
|
||||
|
||||
this.$store.dispatch('maps/resetFilters');
|
||||
this.$store.dispatch('datasets/resetFilters');
|
||||
|
||||
this.$store.dispatch('maps/setResultsPerPage', 6);
|
||||
this.$store.dispatch('datasets/setResultsPerPage', 6);
|
||||
|
||||
// If user is viewer, show shared maps and datasets
|
||||
if (this.$store.getters['user/isViewer']) {
|
||||
this.$store.dispatch('maps/filter', 'shared');
|
||||
this.$store.dispatch('datasets/filter', 'shared');
|
||||
}
|
||||
|
||||
this.$store.dispatch('maps/fetch');
|
||||
this.$store.dispatch('datasets/fetch');
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
activeSection: this.$route.query.section || 'RecentSection'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasRecentContent () {
|
||||
return this.$store.getters['recentContent/hasRecentContent'] ||
|
||||
this.$store.state.recentContent.isFetching;
|
||||
},
|
||||
isFirstTimeViewingDashboard () {
|
||||
return this.$store.state.config.isFirstTimeViewingDashboard;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isSectionActive (activeSection) {
|
||||
return activeSection === this.activeSection;
|
||||
},
|
||||
changeSection (nextActiveSection) {
|
||||
this.activeSection = nextActiveSection;
|
||||
},
|
||||
onContentChanged () {
|
||||
this.$store.dispatch('recentContent/fetch');
|
||||
this.$store.dispatch('maps/fetch');
|
||||
this.$store.dispatch('datasets/fetch');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.section {
|
||||
position: relative;
|
||||
|
||||
&--maps {
|
||||
z-index: $z-index__stack-context--first;
|
||||
}
|
||||
|
||||
&--datasets {
|
||||
z-index: $z-index__stack-context--second;
|
||||
}
|
||||
}
|
||||
|
||||
.tags-section {
|
||||
padding-bottom: 104px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<section class="maps-section">
|
||||
<MapsList
|
||||
:hasBulkActions="false"
|
||||
:isCondensedDefault="true"
|
||||
:canChangeViewMode="false"
|
||||
:canHoverCard="false"
|
||||
:maxVisibleMaps="maxVisibleMaps"
|
||||
:isInitialOrEmpty="showViewAllLink"
|
||||
@applyFilter="applyFilter"
|
||||
@applyOrder="applyOrder"
|
||||
@contentChanged="onContentChanged"/>
|
||||
|
||||
<router-link :to="{ name: 'maps' }" class="title is-small viewall-link" v-if="showViewAllLink">{{ mapsLinkText }}</router-link>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import MapsList from 'new-dashboard/components/MapsList.vue';
|
||||
|
||||
export default {
|
||||
name: 'MapsSection',
|
||||
components: {
|
||||
MapsList
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
maxVisibleMaps: 6
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
appliedFilter: state => state.maps.filterType,
|
||||
appliedOrder: state => state.maps.order,
|
||||
isFetchingMaps: state => state.maps.isFetching,
|
||||
numResults: state => state.maps.metadata.total_entries,
|
||||
totalUserEntries: state => state.maps.metadata.total_user_entries,
|
||||
totalShared: state => state.maps.metadata.total_shared,
|
||||
isFirst: state => state.config.isFirstTimeViewingDashboard
|
||||
}),
|
||||
mapsLinkText () {
|
||||
return this.$t('HomePage.MapsSection.viewAll');
|
||||
},
|
||||
showViewAllLink () {
|
||||
return !this.isFetchingMaps && this.totalUserEntries;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
applyFilter (filter) {
|
||||
this.$store.dispatch('maps/filter', filter);
|
||||
this.$store.dispatch('maps/fetch');
|
||||
},
|
||||
applyOrder (orderOptions) {
|
||||
this.$store.dispatch('maps/order', orderOptions);
|
||||
this.$store.dispatch('maps/fetch');
|
||||
},
|
||||
hasFilterApplied (filter) {
|
||||
return this.appliedFilter === filter;
|
||||
},
|
||||
onContentChanged (type) {
|
||||
this.$emit('contentChanged', type);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "new-dashboard/styles/variables";
|
||||
|
||||
.maps-section {
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.viewall-link {
|
||||
display: block;
|
||||
margin-top: 64px;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<QuotaContainer :title="$t(`QuotaSection.account`)" :perMonth="false">
|
||||
<QuotaWidget
|
||||
:name="$t(`QuotaSection.storage`)"
|
||||
:usedQuota="getAmountInUnit(usedStorage, amountExponent)"
|
||||
:availableQuota="getAmountInUnit(availableStorage, amountExponent)"
|
||||
:unit="getUnit(amountExponent)"
|
||||
:formatToLocale="false"
|
||||
:helpLink="storageHelpLink"/>
|
||||
|
||||
<QuotaWidget
|
||||
v-if="hasTableLimits"
|
||||
:name="$t(`QuotaSection.publicMaps`)"
|
||||
:usedQuota="usedPublicMaps"
|
||||
:availableQuota="availablePublicMaps"/>
|
||||
|
||||
<QuotaWidget
|
||||
v-if="hasPublicMapLimits"
|
||||
:name="$t(`QuotaSection.datasets`)"
|
||||
:usedQuota="usedDatasets"
|
||||
:availableQuota="availableDatasets"/>
|
||||
|
||||
<QuotaWidget
|
||||
v-if="hasApiKeysLimits"
|
||||
:name="$t(`QuotaSection.apiKeys`)"
|
||||
:usedQuota="usedApiKeys"
|
||||
:availableQuota="availableApiKeys"/>
|
||||
</QuotaContainer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import QuotaWidget from './QuotaWidget';
|
||||
import QuotaContainer from './QuotaContainer';
|
||||
import { apiKeysTypes } from 'new-dashboard/core/constants/api-keys';
|
||||
import { accountsWithLimits } from 'new-dashboard/core/constants/accounts';
|
||||
|
||||
export default {
|
||||
name: 'AccountQuota',
|
||||
components: {
|
||||
QuotaWidget,
|
||||
QuotaContainer
|
||||
},
|
||||
beforeMount () {
|
||||
this.getApiKeysTotal();
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
usedApiKeys: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
availableStorage: state => state.user.quota_in_bytes,
|
||||
remainingStorage: state => state.user.remaining_byte_quota,
|
||||
availablePublicMaps: state => state.user.public_map_quota,
|
||||
linkMapsTotal: state => state.user.link_privacy_map_count,
|
||||
passwordMapsTotal: state => state.user.password_privacy_map_count,
|
||||
publicMapsTotal: state => state.user.public_privacy_map_count,
|
||||
availableDatasets: state => state.user.table_quota,
|
||||
usedDatasets: state => state.user.table_count,
|
||||
availableApiKeys: state => state.user.regular_api_key_quota,
|
||||
planAccountType: state => state.user.account_type
|
||||
}),
|
||||
usedStorage () {
|
||||
return this.availableStorage - this.remainingStorage;
|
||||
},
|
||||
amountExponent () {
|
||||
return this.getExpBaseTwo(this.availableStorage) - 10;
|
||||
},
|
||||
storageHelpLink () {
|
||||
return 'https://carto.com/help/your-account/your-disk-storage/';
|
||||
},
|
||||
hasTableLimits () {
|
||||
return accountsWithLimits.includes(this.planAccountType);
|
||||
},
|
||||
hasPublicMapLimits () {
|
||||
return accountsWithLimits.includes(this.planAccountType);
|
||||
},
|
||||
hasApiKeysLimits () {
|
||||
return accountsWithLimits.includes(this.planAccountType);
|
||||
},
|
||||
usedPublicMaps () {
|
||||
return this.linkMapsTotal + this.passwordMapsTotal + this.publicMapsTotal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getExpBaseTwo (sizeInBytes) {
|
||||
if (sizeInBytes === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let exponent = 0;
|
||||
if (Math.log2) {
|
||||
exponent = Math.log2(sizeInBytes);
|
||||
} else {
|
||||
exponent = Math.log(sizeInBytes) * Math.LOG2E;
|
||||
}
|
||||
|
||||
return Math.round(exponent / 10) * 10;
|
||||
},
|
||||
getUnit (exponent) {
|
||||
if (exponent < 10) {
|
||||
return 'B';
|
||||
} else if (exponent < 20) {
|
||||
return 'KB';
|
||||
} else if (exponent < 30) {
|
||||
return 'MB';
|
||||
} else if (exponent < 40) {
|
||||
return 'GB';
|
||||
} else if (exponent < 50) {
|
||||
return 'TB';
|
||||
} else if (exponent < 60) {
|
||||
return 'PB';
|
||||
} else if (exponent < 70) {
|
||||
return 'EB';
|
||||
} else {
|
||||
return '?';
|
||||
}
|
||||
},
|
||||
getAmountInUnit (number, exponent) {
|
||||
return number / Math.pow(2, exponent);
|
||||
},
|
||||
getApiKeysTotal () {
|
||||
this.$store.state.client.getApiKeys(apiKeysTypes.REGULAR,
|
||||
|
||||
(err, _, data) => {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
this.usedApiKeys = data.count;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<QuotaContainer :title="$t(`QuotaSection.dataServices`)" :perMonth="true">
|
||||
<QuotaWidget :name="$t(`QuotaSection.geocoding`)" :usedQuota="geocodingUsed" :availableQuota="geocodingAvailable" :helpLink="geocodingHelpLink"></QuotaWidget>
|
||||
<QuotaWidget :name="$t(`QuotaSection.isolines`)" :usedQuota="isolinesUsed" :availableQuota="isolinesAvailable" :helpLink="isolinesHelpLink"></QuotaWidget>
|
||||
<QuotaWidget :name="$t(`QuotaSection.routing`)" :usedQuota="routingUsed" :availableQuota="routingAvailable" :helpLink="routingHelpLink"></QuotaWidget>
|
||||
</QuotaContainer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import QuotaWidget from './QuotaWidget';
|
||||
import QuotaContainer from './QuotaContainer';
|
||||
|
||||
export default {
|
||||
name: 'DataServicesQuota',
|
||||
components: {
|
||||
QuotaWidget,
|
||||
QuotaContainer
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
geocodingUsed: state => state.user.geocoding.monthly_use,
|
||||
geocodingAvailable: state => state.user.geocoding.quota,
|
||||
routingUsed: state => state.user.mapzen_routing.monthly_use,
|
||||
routingAvailable: state => state.user.mapzen_routing.quota ? state.user.mapzen_routing.quota : 0,
|
||||
isolinesUsed: state => state.user.here_isolines.monthly_use,
|
||||
isolinesAvailable: state => state.user.here_isolines.quota
|
||||
}),
|
||||
geocodingHelpLink () {
|
||||
return 'https://carto.com/help/working-with-data/geocoding/';
|
||||
},
|
||||
isolinesHelpLink () {
|
||||
return 'https://carto.com/help/working-with-data/isolines/';
|
||||
},
|
||||
routingHelpLink () {
|
||||
return 'https://carto.com/help/working-with-data/routing/';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="quota-container">
|
||||
<div class="quota-headers">
|
||||
<div class="quota-cell cell--main">
|
||||
<h3 class="title is-caption quota-title">{{title}}</h3>
|
||||
</div>
|
||||
|
||||
<div class="quota-labels">
|
||||
<div class="quota-cell cell--medium">
|
||||
<p class="text is-small is-txtSoftGrey">{{ $t('QuotaSection.header.remaining') }}</p>
|
||||
</div>
|
||||
<div class="quota-cell cell--medium cell--mobile">
|
||||
<p class="text is-small is-txtSoftGrey">{{ $t('QuotaSection.header.used') }}</p>
|
||||
</div>
|
||||
<div class="quota-cell cell--medium">
|
||||
<p class="text is-small is-txtSoftGrey quota-total">
|
||||
{{$t(`QuotaSection.header.total`)}} <span v-if="perMonth" class="quota-month">{{ $t(`QuotaSection.header.perMonth`) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="quota-cell cell--small"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quota-info">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'QuotaContainer',
|
||||
props: {
|
||||
title: String,
|
||||
perMonth: Boolean
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.quota-headers {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.quota-title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.quota-labels {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.quota-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.quota-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:first-of-type {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cell--mobile {
|
||||
@media (max-width: $layout-mobile) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cell--main {
|
||||
min-width: 300px;
|
||||
|
||||
@media (max-width: $layout-mobile) {
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.cell--medium {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.cell--small {
|
||||
width: 58px;
|
||||
|
||||
@media (max-width: $layout-tablet) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.quota-month {
|
||||
&::after {
|
||||
content: '*';
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $layout-tablet) and (min-width: $layout-mobile) {
|
||||
.quota-total {
|
||||
&::after {
|
||||
content: '*';
|
||||
}
|
||||
}
|
||||
|
||||
.quota-month {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="section is-bgSoftBlue section--noBorder">
|
||||
<div class="container">
|
||||
<div class="full-width">
|
||||
<SectionTitle class="grid-cell">
|
||||
<template slot="icon">
|
||||
<img svg-inline src="../../../assets/icons/section-title/quota.svg">
|
||||
</template>
|
||||
<template slot="title">{{ $t('QuotaSection.title') }}</template>
|
||||
</SectionTitle>
|
||||
<ul class="grid quota-list">
|
||||
<li class="grid-cell grid-cell--col12 quota-listitem">
|
||||
<AccountQuota></AccountQuota>
|
||||
</li>
|
||||
<li class="grid-cell grid-cell--col12 quota-listitem">
|
||||
<DataServicesQuota></DataServicesQuota>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="quota-billing">
|
||||
<span class="quota-billingday text is-small is-txtSoftGrey">
|
||||
{{ $tc(`QuotaSection.credits`, remainingDaysUntilLDSRenewal, { remainingDays: remainingDaysUntilLDSRenewal, day: billingDay })}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import SectionTitle from 'new-dashboard/components/SectionTitle';
|
||||
import AccountQuota from './AccountQuota';
|
||||
import DataServicesQuota from './DataServicesQuota';
|
||||
import format from 'date-fns/format';
|
||||
import differenceInDays from 'date-fns/difference_in_days';
|
||||
import zhCN from 'date-fns/locale/zh_cn';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'QuotasModule',
|
||||
components: {
|
||||
SectionTitle,
|
||||
AccountQuota,
|
||||
DataServicesQuota
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
billingPeriod: state => state.user.next_billing_period
|
||||
}),
|
||||
billingDay () {
|
||||
return format(new Date(this.billingPeriod), 'Do');
|
||||
},
|
||||
remainingDaysUntilLDSRenewal () {
|
||||
return differenceInDays(new Date(this.billingPeriod), new Date(),{ locale: zhCN });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.quota-listitem {
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.quota-billing {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.quota-billingday {
|
||||
margin-top: 16px;
|
||||
|
||||
&::before {
|
||||
content: '*';
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="quota-widget" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
|
||||
<div class="quota-main">
|
||||
<div class="quota-cell cell--title">
|
||||
<h4 class="text is-caption is-semibold is-txtGrey">{{name}}</h4>
|
||||
</div>
|
||||
<div class="quota-cell cell--large">
|
||||
<div class="progressbar">
|
||||
<div :class="`progressbar progressbar--${getStatusBar}`" :style="{width: `${getUsedPercent}%`}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quota-data">
|
||||
<div class="quota-cell cell--medium">
|
||||
<span class="text is-small is-txtSoftGrey">
|
||||
{{ formatToLocale ? getNumberInLocaleFormat(roundOneDecimal(remainingQuota)) : roundOneDecimal(remainingQuota) }} {{unit}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="quota-cell cell--medium cell--mobile">
|
||||
<span class="text is-small is-txtSoftGrey">
|
||||
{{ formatToLocale ? getNumberInLocaleFormat(roundOneDecimal(usedQuota)) : roundOneDecimal(usedQuota) }} {{unit}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="quota-cell cell--medium">
|
||||
<span class="text is-small is-txtSoftGrey">
|
||||
{{ formatToLocale ? getNumberInLocaleFormat(roundOneDecimal(availableQuota)) : roundOneDecimal(availableQuota) }} {{unit}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="quota-help cell--small">
|
||||
<a :href="helpLink" v-if="helpLink" target= "_blank"><img svg-inline class="quota-image" :class="{'is-active': active}" src="../../../assets/icons/common/question-mark.svg"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'QuotaCard',
|
||||
props: {
|
||||
name: String,
|
||||
availableQuota: Number,
|
||||
usedQuota: Number,
|
||||
unit: String,
|
||||
billingPeriod: String,
|
||||
formatToLocale: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
helpLink: String
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
active: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
remainingQuota () {
|
||||
const remainingQuota = this.availableQuota - this.usedQuota;
|
||||
return Math.max(0, remainingQuota);
|
||||
},
|
||||
getUsedPercent () {
|
||||
if (this.availableQuota === 0) {
|
||||
return 100;
|
||||
}
|
||||
const width = (this.usedQuota / this.availableQuota) * 100;
|
||||
return width;
|
||||
},
|
||||
getStatusBar () {
|
||||
const usedPercent = this.getUsedPercent;
|
||||
if (usedPercent > 90) {
|
||||
return 'problem';
|
||||
} else if (usedPercent > 60) {
|
||||
return 'warning';
|
||||
} else {
|
||||
return 'good';
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
roundOneDecimal (number) {
|
||||
return Math.round(number * 10) / 10;
|
||||
},
|
||||
getNumberInLocaleFormat (number) {
|
||||
return number.toLocaleString();
|
||||
},
|
||||
onMouseOver () {
|
||||
this.active = true;
|
||||
},
|
||||
onMouseLeave () {
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.quota-widget {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 1px solid $softblue;
|
||||
}
|
||||
}
|
||||
|
||||
.quota-main {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.quota-data {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.quota-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:first-of-type {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cell--mobile {
|
||||
@media (max-width: $layout-mobile) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.quota-help {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-right: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quota-image {
|
||||
display: none;
|
||||
|
||||
&.is-active {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.cell--title {
|
||||
width: 110px;
|
||||
margin-left: 36px;
|
||||
}
|
||||
|
||||
.cell--large {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 120px;
|
||||
margin-right: 40px;
|
||||
|
||||
@media (max-width: $layout-mobile) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cell--medium {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.cell--small {
|
||||
width: 58px;
|
||||
|
||||
@media (max-width: $layout-tablet) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.progressbar {
|
||||
width: 100%;
|
||||
max-width: 240px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: $progressbar__bg-color;
|
||||
|
||||
&.progressbar--good {
|
||||
background-color: $success__bg-color;
|
||||
}
|
||||
|
||||
&.progressbar--warning {
|
||||
background-color: $warning__bg-color;
|
||||
}
|
||||
|
||||
.progressbar--problem {
|
||||
background-color: $danger__bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<section class="recent-section is-bgSoftBlue">
|
||||
<div class="container">
|
||||
<SectionTitle class="grid-cell">
|
||||
<template slot="icon">
|
||||
<img src="../../../assets/icons/section-title/recent.svg">
|
||||
</template>
|
||||
<template slot="title">{{ $t('HomePage.RecentSection.title') }}</template>
|
||||
<template slot="actionButton">
|
||||
<button class="button button--small is-primary button--ghost button--last" @click="goToTagsSection">
|
||||
<img svg-inline src="../../../assets/icons/sections/recent-content/tags.svg" class="recent__action"/>
|
||||
{{ $t('HomePage.RecentSection.viewTagsAction') }}
|
||||
</button>
|
||||
</template>
|
||||
</SectionTitle>
|
||||
|
||||
<ul class="grid">
|
||||
<template v-if="!isFetching">
|
||||
<li class="card grid-cell grid-cell--col4 grid-cell--col6--tablet grid-cell--col12--mobile"
|
||||
v-for="visualization in recentContent"
|
||||
:key="visualization.id">
|
||||
<SimpleMapCard
|
||||
:visualization="visualization"
|
||||
:visibleSections="visibleSections"
|
||||
:canHover="false"
|
||||
:singleLineTitle="true"
|
||||
storeActionType="recentContent"
|
||||
@contentChanged="onContentChanged" />
|
||||
</li>
|
||||
</template>
|
||||
<template v-else>
|
||||
<li class="card grid-cell grid-cell--col4 grid-cell--col6--tablet grid-cell--col12--mobile"
|
||||
v-for="n in 3"
|
||||
:key="n">
|
||||
<SimpleMapCardFake :visibleSections="visibleSections"></SimpleMapCardFake>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import SectionTitle from 'new-dashboard/components/SectionTitle';
|
||||
import SimpleMapCard from 'new-dashboard/components/MapCard/SimpleMapCard';
|
||||
import SimpleMapCardFake from 'new-dashboard/components/MapCard/fakes/SimpleMapCardFake';
|
||||
|
||||
export default {
|
||||
name: 'RecentSection',
|
||||
components: {
|
||||
SectionTitle,
|
||||
SimpleMapCard,
|
||||
SimpleMapCardFake
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
visibleSections: ['privacy', 'lastModification']
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isFetching: state => {
|
||||
return state.recentContent.isFetching;
|
||||
},
|
||||
recentContent: state => state.recentContent.list
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
goToTagsSection () {
|
||||
const section = 'TagsSection';
|
||||
|
||||
this.$emit('sectionChange', section);
|
||||
this.$router.push({ query: { section, sectionPage: 1 } });
|
||||
},
|
||||
|
||||
onContentChanged (type) {
|
||||
this.$emit('contentChanged', type);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.recent__action {
|
||||
margin-right: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<section class="tags-section is-bgSoftBlue">
|
||||
<div class="container">
|
||||
<SectionTitle class="grid-cell">
|
||||
<template slot="icon">
|
||||
<img src="../../../assets/icons/section-title/tags.svg">
|
||||
</template>
|
||||
|
||||
<template slot="title">{{ $t('HomePage.TagsSection.title') }}</template>
|
||||
|
||||
<template slot="actionButton">
|
||||
<button class="button button--small is-primary button--ghost button--last" @click="goToRecentSection">
|
||||
<img svg-inline src="../../../assets/icons/sections/tags/recent_content.svg" class="tags__action"/>
|
||||
{{ $t('HomePage.TagsSection.viewRecentContentAction') }}
|
||||
</button>
|
||||
</template>
|
||||
</SectionTitle>
|
||||
|
||||
<LoadingState v-if="isFetching" :text="$t('HomePage.TagsSection.loading')" class="loading"></LoadingState>
|
||||
|
||||
<EmptyState
|
||||
:text="$t('HomePage.TagsSection.emptyState.title')"
|
||||
:subtitle="$t('HomePage.TagsSection.emptyState.subtitle')"
|
||||
v-if="isEmptyState">
|
||||
</EmptyState>
|
||||
|
||||
<ul class="grid tags" v-if="hasTags">
|
||||
<li class="card grid-cell grid-cell--col4 grid-cell--col6--tablet grid-cell--col12--mobile tag" v-for="tag in tags" :key="tag.tag">
|
||||
<TagCard :tag="tag" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Pagination class="pagination-element" v-if="shouldShowPagination" :page=currentPage :numPages=numPages @pageChange="goToPage"></Pagination>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SectionTitle from 'new-dashboard/components/SectionTitle';
|
||||
import TagCard from 'new-dashboard/components/Tag/TagCard';
|
||||
import Pagination from 'new-dashboard/components/Pagination';
|
||||
import EmptyState from 'new-dashboard/components/States/EmptyState';
|
||||
import LoadingState from 'new-dashboard/components/States/LoadingState';
|
||||
|
||||
export default {
|
||||
name: 'TagsSection',
|
||||
components: {
|
||||
SectionTitle,
|
||||
TagCard,
|
||||
Pagination,
|
||||
EmptyState,
|
||||
LoadingState
|
||||
},
|
||||
mounted () {
|
||||
const page = parseInt(this.$route.query.sectionPage);
|
||||
this.goToPage(page);
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isFetching: false,
|
||||
tags: [],
|
||||
currentPage: 1,
|
||||
perPage: 6,
|
||||
totalTags: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasTags () {
|
||||
return !this.isFetching && this.totalTags > 0;
|
||||
},
|
||||
isEmptyState () {
|
||||
return !this.isFetching && this.totalTags <= 0;
|
||||
},
|
||||
numPages () {
|
||||
return Math.ceil(this.totalTags / this.perPage);
|
||||
},
|
||||
shouldShowPagination () {
|
||||
return this.numPages > 1;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToRecentSection () {
|
||||
const section = 'RecentSection';
|
||||
|
||||
this.setURLParams({ section, sectionPage: 1 });
|
||||
this.$emit('sectionChange', section);
|
||||
},
|
||||
getTags (options = {}) {
|
||||
this.isFetching = true;
|
||||
|
||||
options.perPage = this.perPage;
|
||||
|
||||
this.$store.state.client.getTags(options,
|
||||
(err, _, tags) => {
|
||||
this.isFetching = false;
|
||||
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tags = tags.result;
|
||||
this.totalTags = tags.total;
|
||||
this.currentPage = options.page;
|
||||
}
|
||||
);
|
||||
},
|
||||
goToPage (page) {
|
||||
this.setURLParams({ sectionPage: page });
|
||||
this.getTags({ page });
|
||||
},
|
||||
setURLParams ({ section = 'TagsSection', sectionPage = 1 }) {
|
||||
this.$router.push({ query: { section, sectionPage } });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tags__action {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
margin: 154px 0 85px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<section class="welcome-section">
|
||||
<WelcomeFirst v-if="isFirst" :name="name" :userType="userType"></WelcomeFirst>
|
||||
<WelcomeCompact v-if="!isFirst" :name="name" :userType="userType">
|
||||
<template v-if="trialEndDate">
|
||||
<span v-html="trialTimeLeft" class="title is-small"></span>
|
||||
<a class="title is-small" :href="accountUpgradeURL" v-if="accountUpgradeURL">
|
||||
{{ $t('HomePage.WelcomeSection.subscribeNow') }}
|
||||
</a>
|
||||
</template>
|
||||
</WelcomeCompact>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import distanceInWordsStrict from 'date-fns/distance_in_words_strict';
|
||||
import WelcomeCompact from './WelcomeCompact';
|
||||
import WelcomeFirst from './WelcomeFirst';
|
||||
import WelcomeBasic from './WelcomeBasic';
|
||||
import { isOrganizationAdmin } from 'new-dashboard/core/models/organization';
|
||||
|
||||
export default {
|
||||
name: 'Welcome',
|
||||
components: {
|
||||
WelcomeBasic,
|
||||
WelcomeCompact,
|
||||
WelcomeFirst
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isFirst: state => state.config.isFirstTimeViewingDashboard,
|
||||
accountUpgradeURL: state => state.config.upgrade_url,
|
||||
trialEndDate: state => state.user.trial_ends_at,
|
||||
user: state => state.user,
|
||||
name: state => state.user.name || state.user.username,
|
||||
organization: state => state.user.organization,
|
||||
notifications: state => state.user.organizationNotifications
|
||||
}),
|
||||
trialTimeLeft () {
|
||||
return this.$t(`HomePage.WelcomeSection.trialMessage`, { date: distanceInWordsStrict(this.trialEndDate, new Date(), { partialMethod: 'round' }) });
|
||||
},
|
||||
userType () {
|
||||
if (this.isOrganizationAdmin()) {
|
||||
return 'organizationAdmin';
|
||||
}
|
||||
|
||||
if (this.isOrganizationUser()) {
|
||||
return 'organizationUser';
|
||||
}
|
||||
|
||||
if (this.isIndividualUser()) {
|
||||
return 'individual';
|
||||
}
|
||||
|
||||
if (this.isPersonal30()) {
|
||||
return '30day';
|
||||
}
|
||||
|
||||
if (this.isFreeUser()) {
|
||||
return 'free';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isPersonal30 () {
|
||||
const personal30User = ['PERSONAL30'];
|
||||
return personal30User.includes(this.user.account_type);
|
||||
},
|
||||
isFreeUser () {
|
||||
const freeUser = ['FREE'];
|
||||
return freeUser.includes(this.user.account_type);
|
||||
},
|
||||
isIndividualUser () {
|
||||
const individualUsers = ['Individual'];
|
||||
return individualUsers.includes(this.user.account_type);
|
||||
},
|
||||
isOrganizationAdmin () {
|
||||
if (!this.isOrganizationUser()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isOrganizationAdmin(this.user.organization, this.user);
|
||||
},
|
||||
isOrganizationUser () {
|
||||
return Boolean(this.organization);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<section class="welcome-basic">
|
||||
<div class="container">
|
||||
<div class="welcome-basic__greeting title is-title">{{ greeting }}</div>
|
||||
<span v-if="hasNotifications" class="notification-led"></span>
|
||||
<div class="welcome-basic__text text is-caption">{{ text }}</div>
|
||||
<div class="welcome-basic__actions">
|
||||
<button v-if="!hasNotifications" class="button button--small is-primary">Learn More</button>
|
||||
<button v-if="hasNotifications" class="button button--small is-primary">Go to Notifications</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WelcomeBasic',
|
||||
props: {
|
||||
username: String,
|
||||
notifications: Array
|
||||
},
|
||||
computed: {
|
||||
greeting () {
|
||||
return this.$t('HomePage.WelcomeSection.greeting', {username: this.$props.username});
|
||||
},
|
||||
text () {
|
||||
return this.hasNotifications
|
||||
? 'You have unread notifications, do you want to read them now?'
|
||||
: 'We just released CARTO VL, our new vector rendering Javascript library to create amazing LI applications.';
|
||||
},
|
||||
hasNotifications () {
|
||||
return this.$props.notifications.length > 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "new-dashboard/styles/variables";
|
||||
|
||||
.welcome-basic {
|
||||
position: relative;
|
||||
padding: 124px 0;
|
||||
text-align: center;
|
||||
|
||||
.notification-led {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: $notification__bg-color;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
&__greeting {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__text {
|
||||
max-width: 724px;
|
||||
margin: 16px auto 48px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<section class="welcome-compact">
|
||||
<div class="container welcome-compact__content">
|
||||
<div class="welcome-compact__greeting title is-caption">{{ greeting }}</div>
|
||||
<div class="welcome-compact__actions">
|
||||
<OnboardingButton :isFirstTimeViewingDashboard="false"></OnboardingButton>
|
||||
<CreateButton class="button--ghost" visualizationType="map" :disabled="isViewer">{{ $t(`HomePage.WelcomeSection.actions.createMap`) }}</CreateButton>
|
||||
<CreateButton class="button--ghost" visualizationType="dataset" :disabled="!canCreateDatasets">{{ $t(`HomePage.WelcomeSection.actions.createDataset`) }}</CreateButton>
|
||||
</div>
|
||||
|
||||
<div class="welcome-compact__extra">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CreateButton from 'new-dashboard/components/CreateButton.vue';
|
||||
import OnboardingButton from 'new-dashboard/components/Onboarding/OnboardingButton.vue';
|
||||
|
||||
export default {
|
||||
name: 'WelcomeCompact',
|
||||
components: {
|
||||
CreateButton,
|
||||
OnboardingButton
|
||||
},
|
||||
props: {
|
||||
name: String
|
||||
},
|
||||
computed: {
|
||||
greeting () {
|
||||
return this.$t('HomePage.WelcomeSection.greeting', {name: this.$props.name});
|
||||
},
|
||||
canCreateDatasets () {
|
||||
return this.$store.getters['user/canCreateDatasets'];
|
||||
},
|
||||
isViewer () {
|
||||
return this.$store.getters['user/isViewer'];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "new-dashboard/styles/variables";
|
||||
|
||||
.welcome-compact__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 940px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.welcome-compact {
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #E6E8EB;
|
||||
|
||||
&__greeting {
|
||||
display: inline-block;
|
||||
margin-right: 64px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
&__extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.button--ghost {
|
||||
margin-right: 48px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: #047AE6;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.button--outline {
|
||||
margin-left: 24px;
|
||||
padding: 9px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<section class="welcome-first">
|
||||
<div class="container">
|
||||
<div class="welcome-first__greeting title is-title">{{ greeting }}</div>
|
||||
<div class="welcome-first__text text is-caption" v-html="text"></div>
|
||||
<div class="welcome-first__actions">
|
||||
<OnboardingButton v-if="!isOrganizationAdmin" :isFirstTimeViewingDashboard="true"></OnboardingButton>
|
||||
<a class="button button--border"
|
||||
:href="`${ baseUrl }/organization`"
|
||||
v-if="isOrganizationAdmin">
|
||||
{{ $t('HomePage.WelcomeSection.firstTime.manageOrganization') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import differenceInDays from 'date-fns/difference_in_days';
|
||||
import CreateButton from 'new-dashboard/components/CreateButton.vue';
|
||||
import OnboardingButton from 'new-dashboard/components/Onboarding/OnboardingButton.vue';
|
||||
|
||||
export default {
|
||||
name: 'WelcomeFirst',
|
||||
components: {
|
||||
CreateButton,
|
||||
OnboardingButton
|
||||
},
|
||||
props: {
|
||||
name: String,
|
||||
userType: String
|
||||
},
|
||||
computed: {
|
||||
greeting () {
|
||||
return this.$t('HomePage.WelcomeSection.greeting', {name: this.$props.name});
|
||||
},
|
||||
text () {
|
||||
const organizationName = this.$store.state.user.organization && this.$store.state.user.organization.name;
|
||||
|
||||
const firstTimeMessage = this.$t('HomePage.WelcomeSection.firstTime.message');
|
||||
const planMessage = this.$t(`HomePage.WelcomeSection.firstTime.planMessage.${this.userType}`, {
|
||||
organizationName,
|
||||
trialLength: this.trialLength
|
||||
});
|
||||
|
||||
return `${firstTimeMessage} ${planMessage}`;
|
||||
},
|
||||
baseUrl () {
|
||||
return this.$store.state.user.base_url;
|
||||
},
|
||||
isOrganizationAdmin () {
|
||||
return this.userType === 'organizationAdmin';
|
||||
},
|
||||
isOrganizationUser () {
|
||||
return this.userType === 'organizationUser';
|
||||
},
|
||||
organizationMail () {
|
||||
const organization = this.$store.state.user.organization;
|
||||
return organization.admin_email;
|
||||
},
|
||||
canCreateDatasets () {
|
||||
return this.$store.getters['user/canCreateDatasets'];
|
||||
},
|
||||
isViewer () {
|
||||
return this.$store.getters['user/isViewer'];
|
||||
},
|
||||
trialLength () {
|
||||
const trialEndDate = this.$store.state.user.trial_ends_at;
|
||||
const createdAt = this.$store.state.user.created_at;
|
||||
return trialEndDate ? differenceInDays(trialEndDate, createdAt) : null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "new-dashboard/styles/variables";
|
||||
|
||||
.welcome-first {
|
||||
position: relative;
|
||||
padding: 124px 0;
|
||||
background: $primary-color;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
|
||||
&__text {
|
||||
max-width: 724px;
|
||||
margin: 16px auto 48px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button.button--border {
|
||||
border: 1px solid $white;
|
||||
background: none;
|
||||
color: $white;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<Page>
|
||||
<div class="container maintenance-container">
|
||||
<div class="maintenance-icon">
|
||||
<img src="../assets/icons/common/wrench.svg" />
|
||||
</div>
|
||||
<h2 class="maintenance-title title is-title is-txtDarkBlue is-bold">{{ $t('MaintenancePage.title') }}</h2>
|
||||
<p class="text is-body is-txtDarkBlue">{{ $t('MaintenancePage.text') }}</p>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Page from 'new-dashboard/components/Page';
|
||||
|
||||
export default {
|
||||
name: 'Maintenance',
|
||||
components: {
|
||||
Page
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.maintenance-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.maintenance-icon {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.maintenance-title {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<Page>
|
||||
<StickySubheader :is-visible="Boolean(selectedMaps.length && isScrollPastHeader)">
|
||||
<h2 class="title is-caption">
|
||||
{{ $t('BulkActions.selected', {count: selectedMaps.length}) }}
|
||||
</h2>
|
||||
|
||||
<MapBulkActions
|
||||
:selectedMaps="selectedMaps"
|
||||
:areAllMapsSelected="areAllMapsSelected"
|
||||
@selectAll="selectAll"
|
||||
@deselectAll="deselectAll"></MapBulkActions>
|
||||
</StickySubheader>
|
||||
|
||||
<MapsList
|
||||
ref="mapsList"
|
||||
class="grid__content"
|
||||
:hasBulkActions="true"
|
||||
:isCondensedDefault="isCondensed"
|
||||
:canChangeViewMode="true"
|
||||
:canHoverCard="true"
|
||||
:maxVisibleMaps="maxVisibleMaps"
|
||||
@applyFilter="applyFilter"
|
||||
@applyOrder="applyOrder"
|
||||
@selectionChange="updateSelected" />
|
||||
|
||||
<Pagination v-if="shouldShowPagination" :page=currentPage :numPages=numPages @pageChange="goToPage"></Pagination>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { checkFilters } from 'new-dashboard/router/hooks/check-navigation';
|
||||
import { mapState } from 'vuex';
|
||||
import Page from 'new-dashboard/components/Page';
|
||||
import MapBulkActions from 'new-dashboard/components/BulkActions/MapBulkActions.vue';
|
||||
import Pagination from 'new-dashboard/components/Pagination';
|
||||
import StickySubheader from 'new-dashboard/components/StickySubheader';
|
||||
import MapsList from 'new-dashboard/components/MapsList.vue';
|
||||
|
||||
export default {
|
||||
name: 'MapsPage',
|
||||
components: {
|
||||
Page,
|
||||
MapBulkActions,
|
||||
StickySubheader,
|
||||
Pagination,
|
||||
MapsList
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isScrollPastHeader: false,
|
||||
maxVisibleMaps: 12,
|
||||
selectedMaps: [],
|
||||
isCondensed: false
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
this.loadUserConfiguration();
|
||||
},
|
||||
beforeMount () {
|
||||
if (this.$store.getters['user/isViewer']) {
|
||||
// Redirect to shared maps page if user is viewer
|
||||
return this.$router.replace({ name: 'maps', params: { filter: 'shared' } });
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.stickyScrollPosition = this.getHeaderBottomPageOffset();
|
||||
this.$onScrollChange = this.onScrollChange.bind(this);
|
||||
document.addEventListener('scroll', this.$onScrollChange, { passive: true });
|
||||
},
|
||||
beforeDestroy () {
|
||||
document.removeEventListener('scroll', this.$onScrollChange, { passive: true });
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
checkFilters('maps', 'maps', to, from, next);
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
numPages: state => state.maps.numPages,
|
||||
currentPage: state => state.maps.page,
|
||||
maps: state => state.maps.list,
|
||||
isFetchingMaps: state => state.maps.isFetching,
|
||||
currentEntriesCount: state => state.maps.metadata.total_entries
|
||||
}),
|
||||
isNotificationVisible () {
|
||||
return this.$store.getters['user/isNotificationVisible'];
|
||||
},
|
||||
areAllMapsSelected () {
|
||||
return Object.keys(this.maps).length === this.selectedMaps.length;
|
||||
},
|
||||
shouldShowPagination () {
|
||||
return !this.isFetchingMaps && this.currentEntriesCount > 0 && this.numPages > 1;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateSelected (selected) {
|
||||
this.selectedMaps = selected;
|
||||
},
|
||||
goToPage (page) {
|
||||
window.scroll({ top: 0, left: 0 });
|
||||
|
||||
this.deselectAll();
|
||||
this.$router.push({
|
||||
name: 'maps',
|
||||
params: this.$route.params,
|
||||
query: { ...this.$route.query, page }
|
||||
});
|
||||
},
|
||||
applyFilter (filter) {
|
||||
this.$router.push({ name: 'maps', params: { filter } });
|
||||
},
|
||||
applyOrder (orderParams) {
|
||||
this.deselectAll();
|
||||
this.$router.push({
|
||||
name: 'maps',
|
||||
params: this.$route.params,
|
||||
query: {
|
||||
...this.$route.query,
|
||||
order: orderParams.order,
|
||||
order_direction: orderParams.direction
|
||||
}
|
||||
});
|
||||
},
|
||||
selectAll () {
|
||||
this.$refs.mapsList.selectAll();
|
||||
},
|
||||
deselectAll () {
|
||||
this.$refs.mapsList.deselectAll();
|
||||
},
|
||||
onScrollChange () {
|
||||
this.isScrollPastHeader = window.pageYOffset > this.stickyScrollPosition;
|
||||
},
|
||||
getHeaderBottomPageOffset () {
|
||||
const headerContainer = this.$refs.mapsList.getHeaderContainer();
|
||||
const headerBoundingClientRect = headerContainer.$el.getBoundingClientRect();
|
||||
const notificationHeight = this.isNotificationVisible ? 60 : 0;
|
||||
return headerBoundingClientRect.top - notificationHeight;
|
||||
},
|
||||
loadUserConfiguration () {
|
||||
if (localStorage.hasOwnProperty('mapViewMode')) {
|
||||
if (localStorage.mapViewMode === 'compact') {
|
||||
this.isCondensed = true;
|
||||
} else if (localStorage.mapViewMode === 'standard') {
|
||||
this.isCondensed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<Page>
|
||||
<div class="notifications-list-container container grid grid__content">
|
||||
<div class="full-width">
|
||||
<SectionTitle class="grid-cell" :showActionButton="false" ref="headerContainer">
|
||||
<template slot="icon">
|
||||
<img src="../assets/icons/section-title/envelope.svg">
|
||||
</template>
|
||||
<template slot="title">{{ pageTitle }}</template>
|
||||
</SectionTitle>
|
||||
<ul v-if="!emptyState" class="notifications-list grid-cell grid-cell--col9 grid-cell--col12--tablet">
|
||||
<li class="notification-item" v-for="notification in notifications" :key="notification.id">
|
||||
<NotificationCard
|
||||
:receivedAt="notification.received_at"
|
||||
:readAt="notification.read_at"
|
||||
:htmlBody="notification.html_body">
|
||||
</NotificationCard>
|
||||
</li>
|
||||
</ul>
|
||||
<EmptyState v-if="!isFetching && emptyState" :text="emptyStateText">
|
||||
<img svg-inline src="../assets/icons/common/check-bubble.svg">
|
||||
</EmptyState>
|
||||
<LoadingState v-if="isFetching" :text="loadingStateText" class="loading-state"></LoadingState>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Page from 'new-dashboard/components/Page';
|
||||
import EmptyState from '../components/States/EmptyState';
|
||||
import LoadingState from '../components/States/LoadingState';
|
||||
import NotificationCard from '../components/NotificationCard';
|
||||
import SectionTitle from '../components/SectionTitle';
|
||||
|
||||
export default {
|
||||
name: 'NotificationsPage',
|
||||
components: {
|
||||
Page,
|
||||
EmptyState,
|
||||
LoadingState,
|
||||
NotificationCard,
|
||||
SectionTitle
|
||||
},
|
||||
computed: {
|
||||
pageTitle () {
|
||||
return this.$t(`NotificationsPage.header.title`);
|
||||
},
|
||||
notifications () {
|
||||
return this.$store.state.notifications.notifications;
|
||||
},
|
||||
emptyState () {
|
||||
return !this.notifications || !this.notifications.length;
|
||||
},
|
||||
emptyStateText () {
|
||||
return this.$t(`NotificationsPage.emptyState`);
|
||||
},
|
||||
loadingStateText () {
|
||||
return this.$t(`NotificationsPage.loadingState`);
|
||||
},
|
||||
isFetching () {
|
||||
return this.$store.state.notifications.isFetching;
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
this.$store.dispatch('user/resetOrganizationNotifications');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "new-dashboard/styles/variables";
|
||||
|
||||
.notifications-list-container {
|
||||
margin-bottom: 44px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
margin-bottom: 36px;
|
||||
border-bottom: 1px solid $softblue;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.notification {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state,
|
||||
.loading-state {
|
||||
margin: 20vh 0 8vh;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,311 @@
|
||||
<template>
|
||||
<Page class="page__sticky-subheader">
|
||||
<StickySubheader :is-visible="true" class="page-subheader">
|
||||
<span class="title" v-if="isFirstFetch">
|
||||
{{ $t('SearchPage.title.allFetching', { query: searchTerm || tag }) }}
|
||||
<span class="loading">
|
||||
<img svg-inline src="../assets/icons/common/loading.svg" class="loading__svg"/>
|
||||
</span>
|
||||
</span>
|
||||
<span class="title" v-else-if="searchTerm">{{ $tc('SearchPage.title.searchTerm', totalResults, { query: searchTerm }) }}</span>
|
||||
<span class="title" v-else-if="tag">{{ $tc('SearchPage.title.tag', totalResults, { query: tag }) }}</span>
|
||||
</StickySubheader>
|
||||
|
||||
<div class="container grid grid__content">
|
||||
<div class="full-width">
|
||||
<section class="section section--maps" :class="{ 'has-pagination': hasMaps && mapsNumPages > 1 }" ref="maps">
|
||||
<div class="section__title grid-cell title is-medium">{{ $t('SearchPage.sections.maps') }}</div>
|
||||
<div class="js-grid__head--sticky">
|
||||
<div v-if="hasMaps || isFetchingMaps"
|
||||
class="grid-cell grid-cell--noMargin grid-cell--col12 grid__head--sticky"
|
||||
:class="{ 'has-user-notification': isNotificationVisible }">
|
||||
<CondensedMapHeader order="" orderDirection="" :isSortable="false"></CondensedMapHeader>
|
||||
</div>
|
||||
|
||||
<ul class="grid-cell grid-cell--col12" v-if="isFetchingMaps">
|
||||
<li v-for="n in 6" :key="n" class="search-item">
|
||||
<MapCardFake :condensed="true" class="search-item"></MapCardFake>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="grid-cell grid-cell--col12" v-if="!isFetchingMaps">
|
||||
<li v-for="map in maps" :key="map.id" class="search-item">
|
||||
<MapCard :visualization=map :canHover=false :condensed="true" storeActionType="search"></MapCard>
|
||||
</li>
|
||||
|
||||
<div class="is-caption text maps--empty" v-if="!hasMaps">
|
||||
{{ $t('SearchPage.emptyText.maps') }}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="hasMaps && mapsNumPages > 1"
|
||||
:page=mapsPage
|
||||
:numPages=mapsNumPages
|
||||
@pageChange="page => onPageChange('maps', page)"></Pagination>
|
||||
</section>
|
||||
|
||||
<section class="section section--datasets" ref="datasets">
|
||||
<div class="section__title grid-cell title is-medium">{{ $t('SearchPage.sections.data') }}</div>
|
||||
<div class="js-grid__head--sticky">
|
||||
<div
|
||||
v-if="hasDatasets || isFetchingDatasets"
|
||||
class="grid-cell grid-cell--noMargin grid-cell--col12 grid__head--sticky"
|
||||
:class="{ 'has-user-notification': isNotificationVisible }">
|
||||
<DatasetListHeader order="" orderDirection="" :isSortable="false"></DatasetListHeader>
|
||||
</div>
|
||||
|
||||
<ul class="grid-cell grid-cell--col12" v-if="!isFetchingDatasets">
|
||||
<li v-for="dataset in datasets" :key="dataset.id" class="search-item">
|
||||
<DatasetCard :dataset=dataset :canHover=false storeActionType="search"></DatasetCard>
|
||||
</li>
|
||||
|
||||
<div class="is-caption text data--empty" v-if="!hasDatasets">
|
||||
{{ $t('SearchPage.emptyText.datasets') }}
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<ul class="grid-cell grid-cell--col12" v-if="isFetchingDatasets">
|
||||
<li v-for="n in 6" :key="n" class="search-item">
|
||||
<DatasetCardFake></DatasetCardFake>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Pagination
|
||||
class="pagination-element"
|
||||
v-if="hasDatasets && datasetsNumPages > 1"
|
||||
:page=datasetsPage
|
||||
:numPages=datasetsNumPages
|
||||
@pageChange="page => onPageChange('datasets', page)"></Pagination>
|
||||
</section>
|
||||
|
||||
<section class="section section--tags" ref="tags" v-if="isTermSearch">
|
||||
<div class="section__title grid-cell title is-medium">{{ $t('SearchPage.sections.tags') }}</div>
|
||||
<ul class="grid" v-if="!isFetchingTags">
|
||||
<li v-for="(tag, n) in tags" :key="n" class="search-item--tag grid-cell grid-cell--col4 grid-cell--col6--tablet grid-cell--col12--mobile">
|
||||
<TagCard :tag=tag :condensed="true"></TagCard>
|
||||
</li>
|
||||
<li class="grid-cell">
|
||||
<div class="is-caption text" v-if="!hasTags">
|
||||
{{ $t('SearchPage.emptyText.tags') }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="grid" v-if="isFetchingTags">
|
||||
<li v-for="n in 6" :key="n" class="search-item--tag grid-cell grid-cell--col4 grid-cell--col6--tablet grid-cell--col12--mobile">
|
||||
<FakeTagCard></FakeTagCard>
|
||||
</li>
|
||||
</ul>
|
||||
<Pagination
|
||||
class="pagination-element"
|
||||
v-if="hasTags && tagsNumPages > 1"
|
||||
:page=tagsPage
|
||||
:numPages=tagsNumPages
|
||||
@pageChange="page => onPageChange('tags', page)"></Pagination>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Page from 'new-dashboard/components/Page';
|
||||
import StickySubheader from 'new-dashboard/components/StickySubheader';
|
||||
import CondensedMapHeader from 'new-dashboard/components/MapCard/CondensedMapHeader.vue';
|
||||
import MapCard from 'new-dashboard/components/MapCard/MapCard.vue';
|
||||
import MapCardFake from 'new-dashboard/components/MapCard/fakes/MapCardFake';
|
||||
import DatasetListHeader from '../components/Dataset/DatasetListHeader';
|
||||
import DatasetCard from 'new-dashboard/components/Dataset/DatasetCard';
|
||||
import DatasetCardFake from 'new-dashboard/components/Dataset/DatasetCardFake';
|
||||
import TagCard from 'new-dashboard/components/Tag/TagCard';
|
||||
import FakeTagCard from 'new-dashboard/components/Tag/FakeTagCard';
|
||||
import Pagination from 'new-dashboard/components/Pagination';
|
||||
import updateSearchParams from 'new-dashboard/router/hooks/update-search-params';
|
||||
import { mapState } from 'vuex';
|
||||
|
||||
const TWO_HEADERS_HEIGHT = 128;
|
||||
|
||||
export default {
|
||||
name: 'SearchPage',
|
||||
components: {
|
||||
Page,
|
||||
CondensedMapHeader,
|
||||
DatasetCard,
|
||||
DatasetCardFake,
|
||||
DatasetListHeader,
|
||||
MapCard,
|
||||
MapCardFake,
|
||||
Pagination,
|
||||
StickySubheader,
|
||||
TagCard,
|
||||
FakeTagCard
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.$store.dispatch('search/resetState');
|
||||
this.isFirstFetch = true;
|
||||
|
||||
this.updateSearchParams(to, from, next);
|
||||
},
|
||||
beforeRouteLeave (to, from, next) {
|
||||
this.$store.dispatch('search/resetState');
|
||||
next();
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isFirstFetch: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
searchTerm: state => state.search.searchTerm,
|
||||
tag: state => state.search.tag,
|
||||
maps: state => state.search.maps.results,
|
||||
mapsPage: state => state.search.maps.page,
|
||||
mapsNumPages: state => state.search.maps.numPages,
|
||||
isFetchingMaps: state => state.search.maps.isFetching,
|
||||
datasets: state => state.search.datasets.results,
|
||||
datasetsPage: state => state.search.datasets.page,
|
||||
datasetsNumPages: state => state.search.datasets.numPages,
|
||||
isFetchingDatasets: state => state.search.datasets.isFetching,
|
||||
tags: state => state.search.tags.results,
|
||||
tagsPage: state => state.search.tags.page,
|
||||
tagsNumPages: state => state.search.tags.numPages,
|
||||
isFetchingTags: state => state.search.tags.isFetching,
|
||||
totalResults: state => state.search.maps.numResults +
|
||||
state.search.datasets.numResults +
|
||||
state.search.tags.numResults
|
||||
}),
|
||||
hasMaps () {
|
||||
return Object.keys(this.maps || {}).length;
|
||||
},
|
||||
hasDatasets () {
|
||||
return Object.keys(this.datasets || {}).length;
|
||||
},
|
||||
hasTags () {
|
||||
return (this.tags || []).length;
|
||||
},
|
||||
allSectionsFetching () {
|
||||
return this.isFetchingMaps || this.isFetchingDatasets || this.isFetchingTags;
|
||||
},
|
||||
isTermSearch () {
|
||||
return this.searchTerm;
|
||||
},
|
||||
isNotificationVisible () {
|
||||
return this.$store.getters['user/isNotificationVisible'];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onPageChange (section, page) {
|
||||
this.$store.dispatch('search/changeSectionPage', { section, page });
|
||||
this.scrollToSection(section);
|
||||
},
|
||||
isOutOfViewport (boundingClientRect) {
|
||||
return boundingClientRect.top < TWO_HEADERS_HEIGHT;
|
||||
},
|
||||
scrollToSection (section) {
|
||||
const sectionBoundingClientRect = this.$refs[section].getBoundingClientRect();
|
||||
const offsetDistance = TWO_HEADERS_HEIGHT + 16; // Two headers + margin
|
||||
|
||||
if (!this.isOutOfViewport(sectionBoundingClientRect)) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.scrollBy({ top: sectionBoundingClientRect.top - offsetDistance, behavior: 'smooth' });
|
||||
},
|
||||
updateSearchParams
|
||||
},
|
||||
watch: {
|
||||
allSectionsFetching (newValue) {
|
||||
if (newValue === false) {
|
||||
this.isFirstFetch = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.section {
|
||||
position: relative;
|
||||
margin-bottom: 48px;
|
||||
padding: 0;
|
||||
border-bottom: 0;
|
||||
|
||||
&--maps {
|
||||
z-index: $z-index__stack-context--first;
|
||||
}
|
||||
|
||||
&--datasets {
|
||||
z-index: $z-index__stack-context--second;
|
||||
}
|
||||
|
||||
&--tags {
|
||||
z-index: $z-index__stack-context--third;
|
||||
|
||||
.search-item--tag {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-pagination,
|
||||
&:last-child {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-subheader {
|
||||
background-color: $softblue;
|
||||
}
|
||||
|
||||
.loading {
|
||||
margin-left: 24px;
|
||||
|
||||
&__svg {
|
||||
width: 18px;
|
||||
vertical-align: text-top;
|
||||
|
||||
g {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid__head--sticky {
|
||||
top: 128px;
|
||||
}
|
||||
|
||||
.grid__head--sticky.has-user-notification {
|
||||
top: 128px + $notification-warning__height;
|
||||
}
|
||||
|
||||
.map-element {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.pagination-element {
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
.search-item {
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.maps--empty,
|
||||
.data--empty {
|
||||
margin-bottom: 64px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<Page class="is-bgSoftBlue">
|
||||
<div class="container grid grid__content">
|
||||
<h3 class="title is-body solutions-title">{{ $t(`SolutionsPage.solutionsTitle`) }}</h3>
|
||||
<p class="text is-caption solutions-description">{{ $t(`SolutionsPage.solutionsDescription`) }}</p>
|
||||
|
||||
<ul class="grid-cell--col12 grid">
|
||||
<li class="grid-cell--col12 solution-element">
|
||||
<SolutionCard
|
||||
:title="$t(`SolutionsPage.sitePlanning.title`)"
|
||||
:description="$t(`SolutionsPage.sitePlanning.description`)"
|
||||
infoUrl="https://carto.com/solutions/site-planning/"
|
||||
demoUrl="https://carto.com/demo/siteplanning-demo/">
|
||||
<template slot="icon"><img svg-inline src="../assets/icons/solutions/site-planning.svg"></template>
|
||||
<template slot="image"><img src="../assets/icons/solutions/screens/site-planning.png"></template>
|
||||
<template slot="demo-logos">
|
||||
<img class="demo-card-logo" src="../assets/icons/solutions/logos/dhl.svg"/>
|
||||
<img class="demo-card-logo" src="../assets/icons/solutions/logos/securitas.svg"/>
|
||||
</template>
|
||||
</SolutionCard>
|
||||
</li>
|
||||
|
||||
<li class="grid-cell--col12 solution-element">
|
||||
<SolutionCard
|
||||
:title="$t(`SolutionsPage.territoryManagement.title`)"
|
||||
:description="$t(`SolutionsPage.territoryManagement.description`)"
|
||||
infoUrl="https://carto.com/solutions/territory-management/"
|
||||
demoUrl="https://carto.com/demo/salesquest-demo/">
|
||||
<template slot="icon"><img svg-inline src="../assets/icons/solutions/territory.svg"></template>
|
||||
<template slot="image"><img src="../assets/icons/solutions/screens/territory.png"></template>
|
||||
<template slot="demo-logos">
|
||||
<img class="demo-card-logo" src="../assets/icons/solutions/logos/fcc.svg"/>
|
||||
<img class="demo-card-logo" src="../assets/icons/solutions/logos/dpd.svg"/>
|
||||
<img class="demo-card-logo" src="../assets/icons/solutions/logos/la-poste.svg"/>
|
||||
</template>
|
||||
</SolutionCard>
|
||||
</li>
|
||||
|
||||
<li class="grid-cell--col12 solution-element">
|
||||
<SolutionCard
|
||||
:title="$t(`SolutionsPage.logisticsOptimization.title`)"
|
||||
:description="$t(`SolutionsPage.logisticsOptimization.description`)"
|
||||
infoUrl="https://carto.com/solutions/logistics-optimization/"
|
||||
demoUrl="https://carto.com/demo/logistic-demo/">
|
||||
<template slot="icon"><img svg-inline src="../assets/icons/solutions/logistics.svg"></template>
|
||||
<template slot="image"><img src="../assets/icons/solutions/screens/delivery.png"></template>
|
||||
<template slot="demo-logos">
|
||||
<img class="demo-card-logo" src="../assets/icons/solutions/logos/bonobos.svg"/>
|
||||
<img class="demo-card-logo" src="../assets/icons/solutions/logos/ikea.svg"/>
|
||||
<img class="demo-card-logo" src="../assets/icons/solutions/logos/sanitas.svg"/>
|
||||
</template>
|
||||
</SolutionCard>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="talk-us-card">
|
||||
<h3 class="title is-medium">{{ $t(`SolutionsPage.talkUsTitle`) }}</h3>
|
||||
<p class="text is-caption card-description">{{ $t(`SolutionsPage.talkUsDescription`) }}</p>
|
||||
<div class="talk-us-button">
|
||||
<a href="https://carto.com/request-live-demo/" target="_blank" class="button u-no-decoration">
|
||||
<span>{{ $t(`SolutionsPage.talkUsButton`) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Page from 'new-dashboard/components/Page';
|
||||
import SolutionCard from 'new-dashboard/components/Solutions/SolutionCard';
|
||||
|
||||
export default {
|
||||
name: 'SolutionsPage',
|
||||
components: {
|
||||
Page,
|
||||
SolutionCard
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
@import 'new-dashboard/styles/variables';
|
||||
|
||||
.solutions-title {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.solutions-description {
|
||||
max-width: 615px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.talk-us-card {
|
||||
width: 100%;
|
||||
margin-top: 64px;
|
||||
padding: 36px 28px;
|
||||
border-radius: 2px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
max-width: 561px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.talk-us-button {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.solution-element {
|
||||
margin-top: 48px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<Page>
|
||||
<ExpirationMessage :expirationDays="expirationDays" :addPaymentsURL="addPaymentsURL"></ExpirationMessage>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Page from 'new-dashboard/components/Page';
|
||||
import ExpirationMessage from 'new-dashboard/components/TrialExpired/ExpirationMessage';
|
||||
|
||||
export default {
|
||||
name: 'TrialExpired',
|
||||
components: {
|
||||
Page,
|
||||
ExpirationMessage
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
addPaymentsURL: window.payments_url,
|
||||
expirationDays: window.expiration_days
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user