web/satellite: access grant list page

WHAT:
access grants list page where all the created access grants will be visible/deletable

WHY:
initial page of new access grant flow

Change-Id: I0b99f15e47295bd0d307dd3aebd9f6dea3ffbb25
This commit is contained in:
VitaliiShpital 2020-11-16 19:35:07 +02:00 committed by Vitalii Shpital
parent fa95c6bbb9
commit e16f02b70d
7 changed files with 506 additions and 64 deletions

View File

@ -1,38 +1,242 @@
// Copyright (C) 2020 Storj Labs, Inc. // Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information. // See LICENSE for copying information.
<template> <template>
<div class="access-grants"> <div class="access-grants">
<h2 class="access-grants__title">Access Grants</h2> <div class="access-grants__title-area">
<EmptyState /> <h2 class="access-grants__title-area__title">Access Grants</h2>
</div> <div class="access-grants__title-area__right">
<VButton
label="Create Access Grant +"
width="203px"
height="44px"
class="access-grants__title-area__right__cta"
/>
</div>
</div>
<div v-if="accessGrantsList.length" class="access-grants-items">
<SortAccessGrantsHeader :on-header-click-callback="onHeaderSectionClickCallback"/>
<div class="access-grants-items__content">
<VList
:data-set="accessGrantsList"
:item-component="itemComponent"
:on-item-click="toggleSelection"
/>
</div>
<VPagination
v-if="totalPageCount > 1"
class="pagination-area"
ref="pagination"
:total-page-count="totalPageCount"
:on-page-click-callback="onPageClick"
/>
</div>
<EmptyState v-else />
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from 'vue-property-decorator'; import { Component, Vue } from 'vue-property-decorator';
import EmptyState from './EmptyState.vue'; import AccessGrantsItem from '@/components/accessGrants/AccessGrantsItem.vue';
import EmptyState from '@/components/accessGrants/EmptyState.vue';
import SortAccessGrantsHeader from '@/components/accessGrants/SortingHeader.vue';
import VButton from '@/components/common/VButton.vue';
import VList from '@/components/common/VList.vue';
import VPagination from '@/components/common/VPagination.vue';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { AccessGrant, AccessGrantsOrderBy } from '@/types/accessGrants';
import { SortDirection } from '@/types/common';
const {
FETCH,
DELETE,
TOGGLE_SELECTION,
CLEAR,
CLEAR_SELECTION,
SET_SEARCH_QUERY,
SET_SORT_BY,
SET_SORT_DIRECTION,
} = ACCESS_GRANTS_ACTIONS;
declare interface ResetPagination {
resetPageIndex(): void;
}
@Component({ @Component({
components: { components: {
EmptyState, EmptyState,
SortAccessGrantsHeader,
VList,
VPagination,
VButton,
}, },
}) })
export default class AccessGrants extends Vue { export default class AccessGrants extends Vue {
private FIRST_PAGE = 1;
/**
* Indicates if delete confirmation state should appear.
*/
private isDeleteClicked: boolean = false;
public $refs!: {
pagination: HTMLElement & ResetPagination;
};
/**
* Lifecycle hook after initial render where list of existing access grants is fetched.
*/
public async mounted(): Promise<void> {
await this.$store.dispatch(FETCH, 1);
}
/**
* Lifecycle hook before component destruction.
* Clears existing access grants selection.
*/
public beforeDestroy(): void {
this.onClearSelection();
}
/**
* Toggles access grant selection.
* @param accessGrant
*/
public async toggleSelection(accessGrant: AccessGrant): Promise<void> {
await this.$store.dispatch(TOGGLE_SELECTION, accessGrant);
}
/**
* Fetches access grants page by clicked index.
* @param index
*/
public async onPageClick(index: number): Promise<void> {
try {
await this.$store.dispatch(FETCH, index);
} catch (error) {
await this.$notify.error(`Unable to fetch Access Grants. ${error.message}`);
}
}
/**
* Used for sorting.
* @param sortBy
* @param sortDirection
*/
public async onHeaderSectionClickCallback(sortBy: AccessGrantsOrderBy, sortDirection: SortDirection): Promise<void> {
await this.$store.dispatch(SET_SORT_BY, sortBy);
await this.$store.dispatch(SET_SORT_DIRECTION, sortDirection);
try {
await this.$store.dispatch(FETCH, this.FIRST_PAGE);
} catch (error) {
await this.$notify.error(`Unable to fetch Access Grants. ${error.message}`);
}
if (this.totalPageCount > 1) {
this.$refs.pagination.resetPageIndex();
}
}
/**
* Deletes selected access grants, fetches updated list and changes area state to default.
*/
private async delete(): Promise<void> {
try {
await this.$store.dispatch(DELETE);
await this.$notify.success(`Access Grant deleted successfully`);
} catch (error) {
await this.$notify.error(error.message);
}
try {
await this.$store.dispatch(FETCH, this.FIRST_PAGE);
} catch (error) {
await this.$notify.error(`Unable to fetch Access Grants. ${error.message}`);
}
this.isDeleteClicked = false;
if (this.totalPageCount > 1) {
this.$refs.pagination.resetPageIndex();
}
}
/**
* Holds on button click login for deleting API key process.
*/
public onDeleteClick(): void {
if (!this.isDeleteClicked) {
this.isDeleteClicked = true;
return;
}
this.delete();
}
/**
* Clears API Keys selection.
*/
public onClearSelection(): void {
this.$store.dispatch(CLEAR_SELECTION);
this.isDeleteClicked = false;
}
/**
* Returns access grants pages count from store.
*/
public get totalPageCount(): number {
return this.$store.state.accessGrantsModule.page.pageCount;
}
/**
* Returns Access Grant item component.
*/
public get itemComponent() {
return AccessGrantsItem;
}
/**
* Returns access grants from store.
*/
public get accessGrantsList(): AccessGrant[] {
return this.$store.state.accessGrantsModule.page.accessGrants;
}
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.access-grants { .access-grants {
position: relative; position: relative;
padding: 40px 30px 55px 30px; padding: 40px 30px 55px 30px;
font-family: 'font_regular', sans-serif; font-family: 'font_regular', sans-serif;
&__title { &__title-area {
font-family: 'font_bold', sans-serif; display: flex;
font-size: 32px; align-items: center;
line-height: 39px; justify-content: space-between;
color: #263549;
margin: 0; &__title {
font-family: 'font_bold', sans-serif;
font-size: 22px;
line-height: 27px;
color: #263549;
margin: 0;
}
}
.access-grants-items {
position: relative;
&__content {
background-color: #fff;
display: flex;
flex-direction: column;
width: calc(100% - 32px);
justify-content: flex-start;
padding: 16px;
border-radius: 0 0 8px 8px;
}
} }
} }
</style> </style>

View File

@ -0,0 +1,115 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="grants-item-container">
<div class="grants-item-container__common-info">
<div class="checkbox-container">
<CheckboxIcon class="checkbox-container__image"/>
</div>
<div class="name-container" :title="itemData.name">
<p class="name">{{ itemData.name }}</p>
</div>
</div>
<div class="grants-item-container__common-info date-item-container">
<p class="date">{{ itemData.localDate() }}</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import CheckboxIcon from '@/../static/images/common/checkbox.svg';
import { AccessGrant } from '@/types/accessGrants';
@Component({
components: {
CheckboxIcon,
},
})
export default class AccessGrantsItem extends Vue {
@Prop({ default: new AccessGrant('', '', new Date(), '')})
private readonly itemData: AccessGrant;
}
</script>
<style scoped lang="scss">
.grants-item-container {
display: flex;
align-items: center;
justify-content: flex-start;
height: 83px;
background-color: #fff;
cursor: pointer;
width: 100%;
&:hover {
background-color: rgba(242, 244, 247, 0.6);
}
&__common-info {
display: flex;
align-items: center;
justify-content: flex-start;
width: 60%;
}
}
.checkbox-container {
margin-left: 28px;
max-height: 23px;
border-radius: 4px;
}
.name-container {
max-width: calc(100% - 131px);
margin-right: 15px;
}
.name {
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 21px;
color: #354049;
margin-left: 17px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.date {
font-family: 'font_regular', sans-serif;
font-size: 16px;
line-height: 21px;
color: #354049;
margin: 0;
}
.grants-item-container.selected {
background-color: rgba(242, 244, 247, 0.6);
.grants-item-container__common-info {
.checkbox-container {
background-image: url('../../../static/images/accessGrants/vector.png');
background-repeat: no-repeat;
background-size: 18px 12px;
background-position: center;
background-color: #0068dc;
&__image {
&__rect {
stroke: #fff;
}
}
}
}
}
.date-item-container {
width: 40%;
}
</style>

View File

@ -2,19 +2,19 @@
// See LICENSE for copying information. // See LICENSE for copying information.
<template> <template>
<div class="empty-state"> <div class="empty-state">
<div class="empty-state__modal"> <div class="empty-state__modal">
<Key /> <Key />
<h4 class="empty-state__modal__heading">Create Your First Access Grant</h4> <h4 class="empty-state__modal__heading">Create Your First Access Grant</h4>
<p class="empty-state__modal__subheading">Get started by creating an Access to interact with your Buckets</p> <p class="empty-state__modal__subheading">Get started by creating an Access to interact with your Buckets</p>
<VButton <VButton
label="Create Access Grant +" label="Create Access Grant +"
width="199px" width="199px"
height="44px" height="44px"
class="empty-state__modal__cta" class="empty-state__modal__cta"
/> />
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -30,46 +30,44 @@ import Key from '@/../static/images/accessGrants/key.svg';
VButton, VButton,
}, },
}) })
export default class EmptyState extends Vue {}
export default class EmptyState extends Vue {
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.empty-state { .empty-state {
background-image: url('../../../static/images/accessGrants/access-grants-bg.png'); background-image: url('../../../static/images/accessGrants/access-grants-bg.png');
background-size: contain; background-size: contain;
margin-top: 40px; margin-top: 40px;
&__modal { &__modal {
display: block; display: block;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
width: 660px; width: 660px;
text-align: center; text-align: center;
background: #fff; background: #fff;
padding: 100px 30px; padding: 100px 30px;
position: relative; position: relative;
top: 110px; top: 110px;
&__heading { &__heading {
font-family: 'font_bold', sans-serif; font-family: 'font_bold', sans-serif;
font-size: 28px; font-size: 28px;
font-weight: 700; font-weight: 700;
line-height: 16px; line-height: 16px;
margin-bottom: 30px; margin-bottom: 30px;
} }
&__subheading { &__subheading {
font-family: 'font_regular', sans-serif; font-family: 'font_regular', sans-serif;
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
line-height: 21px; line-height: 21px;
} }
&__cta { &__cta {
margin: 25px auto 0; margin: 25px auto 0;
}
} }
} }
}
</style> </style>

View File

@ -6,8 +6,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Vue } from 'vue-property-decorator'; import { Component, Vue } from 'vue-property-decorator';
export default class ProgressBar extends Vue { @Component
} export default class ProgressBar extends Vue {}
</script> </script>

View File

@ -0,0 +1,116 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="sort-header-container">
<div class="sort-header-container__name-item" @click="onHeaderItemClick(AccessGrantsOrderBy.NAME)">
<p class="sort-header-container__name-item__title">NAME</p>
<VerticalArrows
:is-active="areAccessGrantsSortedByName"
:direction="getSortDirection"
/>
</div>
<div class="sort-header-container__date-item" @click="onHeaderItemClick(AccessGrantsOrderBy.CREATED_AT)">
<p class="sort-header-container__date-item__title creation-date">DATE CREATED</p>
<VerticalArrows
:is-active="!areAccessGrantsSortedByName"
:direction="getSortDirection"
/>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import VerticalArrows from '@/components/common/VerticalArrows.vue';
import { AccessGrantsOrderBy, OnHeaderClickCallback } from '@/types/accessGrants';
import { SortDirection } from '@/types/common';
@Component({
components: {
VerticalArrows,
},
})
export default class SortAccessGrantsHeader extends Vue {
@Prop({default: () => new Promise(() => false)})
private readonly onHeaderClickCallback: OnHeaderClickCallback;
public sortBy: AccessGrantsOrderBy = AccessGrantsOrderBy.NAME;
public sortDirection: SortDirection = SortDirection.ASCENDING;
/**
* Used for arrow styling.
*/
public get getSortDirection(): SortDirection {
return this.sortDirection === SortDirection.DESCENDING ? SortDirection.ASCENDING : SortDirection.DESCENDING;
}
public get areAccessGrantsSortedByName(): boolean {
return this.sortBy === AccessGrantsOrderBy.NAME;
}
/**
* Sets sorting kind if different from current.
* If same, changes sort direction.
* @param sortBy
*/
public async onHeaderItemClick(sortBy: AccessGrantsOrderBy): Promise<void> {
if (this.sortBy !== sortBy) {
this.sortBy = sortBy;
this.sortDirection = SortDirection.ASCENDING;
await this.onHeaderClickCallback(this.sortBy, this.sortDirection);
return;
}
this.sortDirection = this.sortDirection === SortDirection.DESCENDING ?
SortDirection.ASCENDING
: SortDirection.DESCENDING;
await this.onHeaderClickCallback(this.sortBy, this.sortDirection);
}
}
</script>
<style scoped lang="scss">
.sort-header-container {
display: flex;
width: calc(100% - 32px);
height: 40px;
background-color: #fff;
margin-top: 31px;
padding: 16px 16px 0 16px;
border-radius: 8px 8px 0 0;
&__name-item,
&__date-item {
width: 60%;
display: flex;
align-items: center;
margin: 0;
cursor: pointer;
&__title {
font-family: 'font_medium', sans-serif;
font-size: 16px;
margin: 0 0 0 23px;
color: #2a2a32;
}
.creation-date {
margin-left: 0;
}
}
&__date-item {
width: 40%;
&__title {
margin: 0;
}
}
}
</style>

View File

@ -51,6 +51,7 @@ import NoPaywallInfoBar from '@/components/noPaywallInfoBar/NoPaywallInfoBar.vue
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized'; import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { RouteConfig } from '@/router'; import { RouteConfig } from '@/router';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { API_KEYS_ACTIONS } from '@/store/modules/apiKeys'; import { API_KEYS_ACTIONS } from '@/store/modules/apiKeys';
import { BUCKET_ACTIONS } from '@/store/modules/buckets'; import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments'; import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
@ -87,6 +88,8 @@ const {
}, },
}) })
export default class DashboardArea extends Vue { export default class DashboardArea extends Vue {
private FIRST_PAGE: number = 1;
/** /**
* Holds router link to project dashboard page. * Holds router link to project dashboard page.
*/ */
@ -176,7 +179,7 @@ export default class DashboardArea extends Vue {
let apiKeysPage: ApiKeysPage = new ApiKeysPage(); let apiKeysPage: ApiKeysPage = new ApiKeysPage();
try { try {
apiKeysPage = await this.$store.dispatch(API_KEYS_ACTIONS.FETCH, 1); apiKeysPage = await this.$store.dispatch(API_KEYS_ACTIONS.FETCH, this.FIRST_PAGE);
} catch (error) { } catch (error) {
await this.$notify.error(`Unable to fetch api keys. ${error.message}`); await this.$notify.error(`Unable to fetch api keys. ${error.message}`);
} }
@ -193,9 +196,15 @@ export default class DashboardArea extends Vue {
return; return;
} }
try {
await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.FETCH, this.FIRST_PAGE);
} catch (error) {
await this.$notify.error(`Unable to fetch api keys. ${error.message}`);
}
await this.$store.dispatch(PM_ACTIONS.SET_SEARCH_QUERY, ''); await this.$store.dispatch(PM_ACTIONS.SET_SEARCH_QUERY, '');
try { try {
await this.$store.dispatch(PM_ACTIONS.FETCH, 1); await this.$store.dispatch(PM_ACTIONS.FETCH, this.FIRST_PAGE);
} catch (error) { } catch (error) {
await this.$notify.error(`Unable to fetch project members. ${error.message}`); await this.$notify.error(`Unable to fetch project members. ${error.message}`);
} }
@ -207,7 +216,7 @@ export default class DashboardArea extends Vue {
} }
try { try {
await this.$store.dispatch(BUCKET_ACTIONS.FETCH, 1); await this.$store.dispatch(BUCKET_ACTIONS.FETCH, this.FIRST_PAGE);
} catch (error) { } catch (error) {
await this.$notify.error(`Unable to fetch buckets. ${error.message}`); await this.$notify.error(`Unable to fetch buckets. ${error.message}`);
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B