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:
parent
fa95c6bbb9
commit
e16f02b70d
@ -2,37 +2,241 @@
|
||||
// See LICENSE for copying information.
|
||||
<template>
|
||||
<div class="access-grants">
|
||||
<h2 class="access-grants__title">Access Grants</h2>
|
||||
<EmptyState />
|
||||
<div class="access-grants__title-area">
|
||||
<h2 class="access-grants__title-area__title">Access Grants</h2>
|
||||
<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>
|
||||
|
||||
<script lang="ts">
|
||||
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({
|
||||
components: {
|
||||
EmptyState,
|
||||
SortAccessGrantsHeader,
|
||||
VList,
|
||||
VPagination,
|
||||
VButton,
|
||||
},
|
||||
})
|
||||
|
||||
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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.access-grants {
|
||||
position: relative;
|
||||
padding: 40px 30px 55px 30px;
|
||||
font-family: 'font_regular', sans-serif;
|
||||
|
||||
&__title-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&__title {
|
||||
font-family: 'font_bold', sans-serif;
|
||||
font-size: 32px;
|
||||
line-height: 39px;
|
||||
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>
|
||||
|
115
web/satellite/src/components/accessGrants/AccessGrantsItem.vue
Normal file
115
web/satellite/src/components/accessGrants/AccessGrantsItem.vue
Normal 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>
|
@ -30,13 +30,11 @@ import Key from '@/../static/images/accessGrants/key.svg';
|
||||
VButton,
|
||||
},
|
||||
})
|
||||
|
||||
export default class EmptyState extends Vue {
|
||||
}
|
||||
export default class EmptyState extends Vue {}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.empty-state {
|
||||
.empty-state {
|
||||
background-image: url('../../../static/images/accessGrants/access-grants-bg.png');
|
||||
background-size: contain;
|
||||
margin-top: 40px;
|
||||
@ -71,5 +69,5 @@ export default class EmptyState extends Vue {
|
||||
margin: 25px auto 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -6,8 +6,8 @@
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
116
web/satellite/src/components/accessGrants/SortingHeader.vue
Normal file
116
web/satellite/src/components/accessGrants/SortingHeader.vue
Normal 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>
|
@ -51,6 +51,7 @@ import NoPaywallInfoBar from '@/components/noPaywallInfoBar/NoPaywallInfoBar.vue
|
||||
|
||||
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
|
||||
import { RouteConfig } from '@/router';
|
||||
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
|
||||
import { API_KEYS_ACTIONS } from '@/store/modules/apiKeys';
|
||||
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
|
||||
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
|
||||
@ -87,6 +88,8 @@ const {
|
||||
},
|
||||
})
|
||||
export default class DashboardArea extends Vue {
|
||||
private FIRST_PAGE: number = 1;
|
||||
|
||||
/**
|
||||
* Holds router link to project dashboard page.
|
||||
*/
|
||||
@ -176,7 +179,7 @@ export default class DashboardArea extends Vue {
|
||||
let apiKeysPage: ApiKeysPage = new ApiKeysPage();
|
||||
|
||||
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) {
|
||||
await this.$notify.error(`Unable to fetch api keys. ${error.message}`);
|
||||
}
|
||||
@ -193,9 +196,15 @@ export default class DashboardArea extends Vue {
|
||||
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, '');
|
||||
try {
|
||||
await this.$store.dispatch(PM_ACTIONS.FETCH, 1);
|
||||
await this.$store.dispatch(PM_ACTIONS.FETCH, this.FIRST_PAGE);
|
||||
} catch (error) {
|
||||
await this.$notify.error(`Unable to fetch project members. ${error.message}`);
|
||||
}
|
||||
@ -207,7 +216,7 @@ export default class DashboardArea extends Vue {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$store.dispatch(BUCKET_ACTIONS.FETCH, 1);
|
||||
await this.$store.dispatch(BUCKET_ACTIONS.FETCH, this.FIRST_PAGE);
|
||||
} catch (error) {
|
||||
await this.$notify.error(`Unable to fetch buckets. ${error.message}`);
|
||||
}
|
||||
|
BIN
web/satellite/static/images/accessGrants/vector.png
Normal file
BIN
web/satellite/static/images/accessGrants/vector.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 179 B |
Loading…
Reference in New Issue
Block a user