web/satellite: show project invitations in all projects dashboard
This change allows users to view and interact with their project member invitations from within the All Projects Dashboard. References #5855 Change-Id: If5b4af46924530c91f8a5c16bfb5134de313dc90
This commit is contained in:
parent
6359c537c0
commit
cafa6db971
@ -6,11 +6,13 @@ import {
|
|||||||
DataStamp,
|
DataStamp,
|
||||||
Project,
|
Project,
|
||||||
ProjectFields,
|
ProjectFields,
|
||||||
|
ProjectInvitation,
|
||||||
ProjectLimits,
|
ProjectLimits,
|
||||||
ProjectsApi,
|
ProjectsApi,
|
||||||
ProjectsCursor,
|
ProjectsCursor,
|
||||||
ProjectsPage,
|
ProjectsPage,
|
||||||
ProjectsStorageBandwidthDaily,
|
ProjectsStorageBandwidthDaily,
|
||||||
|
ProjectInvitationResponse,
|
||||||
} from '@/types/projects';
|
} from '@/types/projects';
|
||||||
import { HttpClient } from '@/utils/httpClient';
|
import { HttpClient } from '@/utils/httpClient';
|
||||||
import { Time } from '@/utils/time';
|
import { Time } from '@/utils/time';
|
||||||
@ -137,7 +139,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
|
|||||||
* Get project limits.
|
* Get project limits.
|
||||||
*
|
*
|
||||||
* @param projectId- project ID
|
* @param projectId- project ID
|
||||||
* throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public async getLimits(projectId: string): Promise<ProjectLimits> {
|
public async getLimits(projectId: string): Promise<ProjectLimits> {
|
||||||
const path = `${this.ROOT_PATH}/${projectId}/usage-limits`;
|
const path = `${this.ROOT_PATH}/${projectId}/usage-limits`;
|
||||||
@ -165,7 +167,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
|
|||||||
/**
|
/**
|
||||||
* Get total limits for all the projects that user owns.
|
* Get total limits for all the projects that user owns.
|
||||||
*
|
*
|
||||||
* throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public async getTotalLimits(): Promise<ProjectLimits> {
|
public async getTotalLimits(): Promise<ProjectLimits> {
|
||||||
const path = `${this.ROOT_PATH}/usage-limits`;
|
const path = `${this.ROOT_PATH}/usage-limits`;
|
||||||
@ -192,7 +194,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
|
|||||||
* @param projectId- project ID
|
* @param projectId- project ID
|
||||||
* @param start- since date
|
* @param start- since date
|
||||||
* @param end- before date
|
* @param end- before date
|
||||||
* throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public async getDailyUsage(projectId: string, start: Date, end: Date): Promise<ProjectsStorageBandwidthDaily> {
|
public async getDailyUsage(projectId: string, start: Date, end: Date): Promise<ProjectsStorageBandwidthDaily> {
|
||||||
const since = Time.toUnixTimestamp(start).toString();
|
const since = Time.toUnixTimestamp(start).toString();
|
||||||
@ -202,7 +204,6 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Can not get project daily usage');
|
throw new Error('Can not get project daily usage');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const usage = await response.json();
|
const usage = await response.json();
|
||||||
@ -272,6 +273,45 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
|
|||||||
return this.getProjectsPage(response.data.ownedProjects);
|
return this.getProjectsPage(response.data.ownedProjects);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a user's pending project member invitations.
|
||||||
|
*
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
public async getUserInvitations(): Promise<ProjectInvitation[]> {
|
||||||
|
const path = `${this.ROOT_PATH}/invitations`;
|
||||||
|
const response = await this.http.get(path);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return result.map(jsonInvite => new ProjectInvitation(
|
||||||
|
jsonInvite.projectID,
|
||||||
|
jsonInvite.projectName,
|
||||||
|
jsonInvite.projectDescription,
|
||||||
|
jsonInvite.inviterEmail,
|
||||||
|
new Date(jsonInvite.createdAt),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(result.error || 'Failed to get project invitations');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles accepting or declining a user's project member invitation.
|
||||||
|
*
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
public async respondToInvitation(projectID: string, response: ProjectInvitationResponse): Promise<void> {
|
||||||
|
const path = `${this.ROOT_PATH}/invitations/${projectID}/respond`;
|
||||||
|
const body = { projectID, response };
|
||||||
|
const httpResponse = await this.http.post(path, JSON.stringify(body));
|
||||||
|
|
||||||
|
if (httpResponse.ok) return;
|
||||||
|
|
||||||
|
const result = await httpResponse.json();
|
||||||
|
throw new Error(result.error || 'Failed to respond to project invitation');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method for mapping projects page from json to ProjectsPage type.
|
* Method for mapping projects page from json to ProjectsPage type.
|
||||||
*
|
*
|
||||||
@ -294,5 +334,4 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
|
|||||||
|
|
||||||
return new ProjectsPage(projects, page.limit, page.offset, page.pageCount, page.currentPage, page.totalCount);
|
return new ProjectsPage(projects, page.limit, page.offset, page.pageCount, page.currentPage, page.totalCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
<span class="label" :class="{uppercase: isUppercase}">
|
<span class="label" :class="{uppercase: isUppercase}">
|
||||||
<component :is="iconComponent" v-if="iconComponent" />
|
<component :is="iconComponent" v-if="iconComponent" />
|
||||||
<span v-if="icon !== 'none'"> </span>
|
<span v-if="icon !== 'none'"> </span>
|
||||||
|
<slot />
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</span>
|
</span>
|
||||||
<div class="icon-wrapper-right">
|
<div class="icon-wrapper-right">
|
||||||
@ -86,7 +87,7 @@ const props = withDefaults(defineProps<{
|
|||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
}>(), {
|
}>(), {
|
||||||
link: undefined,
|
link: undefined,
|
||||||
label: 'Default',
|
label: '',
|
||||||
width: 'inherit',
|
width: 'inherit',
|
||||||
height: 'inherit',
|
height: 'inherit',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
|
172
web/satellite/src/components/modals/JoinProjectModal.vue
Normal file
172
web/satellite/src/components/modals/JoinProjectModal.vue
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
// Copyright (C) 2023 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VModal :on-close="closeModal">
|
||||||
|
<template #content>
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal__header">
|
||||||
|
<Icon />
|
||||||
|
<span class="modal__header__title">Join project</span>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="modal__info">
|
||||||
|
Join the {{ invite.projectName }} team project.
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="modal__buttons">
|
||||||
|
<VButton
|
||||||
|
class="modal__buttons__button"
|
||||||
|
width="calc(50% - 8px)"
|
||||||
|
border-radius="8px"
|
||||||
|
font-size="14px"
|
||||||
|
:is-transparent="true"
|
||||||
|
:is-disabled="isLoading"
|
||||||
|
:on-press="() => respondToInvitation(ProjectInvitationResponse.Decline)"
|
||||||
|
label="Decline"
|
||||||
|
/>
|
||||||
|
<VButton
|
||||||
|
class="modal__buttons__button"
|
||||||
|
width="calc(50% - 8px)"
|
||||||
|
border-radius="8px"
|
||||||
|
font-size="14px"
|
||||||
|
:is-disabled="isLoading"
|
||||||
|
:on-press="() => respondToInvitation(ProjectInvitationResponse.Accept)"
|
||||||
|
label="Join Project"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { useAppStore } from '@/store/modules/appStore';
|
||||||
|
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||||
|
import { useNotify } from '@/utils/hooks';
|
||||||
|
import { ProjectInvitation, ProjectInvitationResponse } from '@/types/projects';
|
||||||
|
import { AnalyticsHttpApi } from '@/api/analytics';
|
||||||
|
import { LocalData } from '@/utils/localData';
|
||||||
|
import { RouteConfig } from '@/router';
|
||||||
|
|
||||||
|
import VModal from '@/components/common/VModal.vue';
|
||||||
|
import VButton from '@/components/common/VButton.vue';
|
||||||
|
|
||||||
|
import Icon from '@/../static/images/modals/boxesIcon.svg';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const projectsStore = useProjectsStore();
|
||||||
|
const notify = useNotify();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
|
||||||
|
|
||||||
|
const isLoading = ref<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns selected project member invitation from the store.
|
||||||
|
*/
|
||||||
|
const invite = computed((): ProjectInvitation => {
|
||||||
|
return projectsStore.state.selectedInvitation;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles accepting or declining the project member invitation.
|
||||||
|
*/
|
||||||
|
async function respondToInvitation(response: ProjectInvitationResponse): Promise<void> {
|
||||||
|
if (isLoading.value) return;
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
let success = false;
|
||||||
|
try {
|
||||||
|
await projectsStore.respondToInvitation(invite.value.projectID, response);
|
||||||
|
success = true;
|
||||||
|
} catch (error) {
|
||||||
|
const action = response === ProjectInvitationResponse.Accept ? 'accept' : 'decline';
|
||||||
|
notify.error(`Failed to ${action} project invitation. ${error.message}`, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await projectsStore.getUserInvitations();
|
||||||
|
await projectsStore.getProjects();
|
||||||
|
} catch (error) {
|
||||||
|
notify.error(`Failed to reload projects and invitations list. ${error.message}`, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
isLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response === ProjectInvitationResponse.Accept) {
|
||||||
|
projectsStore.selectProject(invite.value.projectID);
|
||||||
|
LocalData.setSelectedProjectId(invite.value.projectID);
|
||||||
|
|
||||||
|
analytics.pageVisit(RouteConfig.ProjectDashboard.path);
|
||||||
|
router.push(RouteConfig.ProjectDashboard.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes modal.
|
||||||
|
*/
|
||||||
|
function closeModal(): void {
|
||||||
|
appStore.removeActiveModal();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.modal {
|
||||||
|
width: 410px;
|
||||||
|
padding: 32px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
@media screen and (width <= 460px) {
|
||||||
|
width: calc(100vw - 48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-family: 'font_bold', sans-serif;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 31px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
font-family: 'font_regular', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
padding: 12px 0;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > hr {
|
||||||
|
height: 1px;
|
||||||
|
border: none;
|
||||||
|
background-color: var(--c-grey-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -2,24 +2,39 @@
|
|||||||
// See LICENSE for copying information.
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="tag" :class="{member: !isOwner}">
|
<div class="tag" :class="{owner: isOwner, invited: isInvited}">
|
||||||
<box-icon v-if="!noIcon" class="tag__icon" />
|
<component :is="icon" v-if="!noIcon" class="tag__icon" />
|
||||||
|
|
||||||
<span class="tag__text"> {{ isOwner ? 'Owner': 'Member' }} </span>
|
<span class="tag__text">{{ label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BoxIcon from '@/../static/images/allDashboard/box.svg';
|
import { computed, Component } from 'vue';
|
||||||
|
|
||||||
|
import BoxIcon from '@/../static/images/navigation/project.svg';
|
||||||
|
import InviteIcon from '@/../static/images/navigation/quickStart.svg';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
isOwner: boolean,
|
isOwner: boolean,
|
||||||
|
isInvited: boolean,
|
||||||
noIcon?: boolean,
|
noIcon?: boolean,
|
||||||
}>(), {
|
}>(), {
|
||||||
isOwner: false,
|
isOwner: false,
|
||||||
|
isInvited: false,
|
||||||
noIcon: false,
|
noIcon: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const icon = computed((): string => {
|
||||||
|
return props.isInvited ? InviteIcon : BoxIcon;
|
||||||
|
});
|
||||||
|
|
||||||
|
const label = computed((): string => {
|
||||||
|
if (props.isOwner) return 'Owner';
|
||||||
|
if (props.isInvited) return 'Invited';
|
||||||
|
return 'Member';
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -29,21 +44,39 @@ const props = withDefaults(defineProps<{
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: 1px solid var(--c-purple-2);
|
border: 1px solid var(--c-yellow-2);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
color: var(--c-purple-4);
|
color: var(--c-yellow-5);
|
||||||
|
|
||||||
|
:deep(path) {
|
||||||
|
fill: var(--c-yellow-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
&__text {
|
&__text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-family: 'font_regular', sans-serif;
|
font-family: 'font_regular', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.member {
|
&.owner {
|
||||||
color: var(--c-yellow-5);
|
color: var(--c-purple-4);
|
||||||
border-color: var(--c-yellow-2);
|
border-color: var(--c-purple-2);
|
||||||
|
|
||||||
:deep(path) {
|
:deep(path) {
|
||||||
fill: var(--c-yellow-5);
|
fill: var(--c-purple-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.invited {
|
||||||
|
color: var(--c-grey-6);
|
||||||
|
border-color: var(--c-grey-4);
|
||||||
|
|
||||||
|
:deep(path) {
|
||||||
|
fill: var(--c-yellow-3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,14 @@ import {
|
|||||||
ProjectsPage,
|
ProjectsPage,
|
||||||
ProjectsStorageBandwidthDaily,
|
ProjectsStorageBandwidthDaily,
|
||||||
ProjectUsageDateRange,
|
ProjectUsageDateRange,
|
||||||
|
ProjectInvitation,
|
||||||
|
ProjectInvitationResponse,
|
||||||
} from '@/types/projects';
|
} from '@/types/projects';
|
||||||
import { ProjectsApiGql } from '@/api/projects';
|
import { ProjectsApiGql } from '@/api/projects';
|
||||||
import { DEFAULT_PAGE_LIMIT } from '@/types/pagination';
|
import { DEFAULT_PAGE_LIMIT } from '@/types/pagination';
|
||||||
|
|
||||||
const defaultSelectedProject = new Project('', '', '', '', '', true, 0);
|
const defaultSelectedProject = new Project('', '', '', '', '', true, 0);
|
||||||
|
const defaultSelectedInvitation = new ProjectInvitation('', '', '', '', new Date());
|
||||||
|
|
||||||
export class ProjectsState {
|
export class ProjectsState {
|
||||||
public projects: Project[] = [];
|
public projects: Project[] = [];
|
||||||
@ -32,6 +35,8 @@ export class ProjectsState {
|
|||||||
public storageChartData: DataStamp[] = [];
|
public storageChartData: DataStamp[] = [];
|
||||||
public chartDataSince: Date = new Date();
|
public chartDataSince: Date = new Date();
|
||||||
public chartDataBefore: Date = new Date();
|
public chartDataBefore: Date = new Date();
|
||||||
|
public invitations: ProjectInvitation[] = [];
|
||||||
|
public selectedInvitation: ProjectInvitation = defaultSelectedInvitation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useProjectsStore = defineStore('projects', () => {
|
export const useProjectsStore = defineStore('projects', () => {
|
||||||
@ -202,6 +207,22 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
return await api.getSalt(projectID);
|
return await api.getSalt(projectID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getUserInvitations(): Promise<ProjectInvitation[]> {
|
||||||
|
const invites = await api.getUserInvitations();
|
||||||
|
|
||||||
|
state.invitations = invites;
|
||||||
|
|
||||||
|
return invites;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function respondToInvitation(projectID: string, response: ProjectInvitationResponse): Promise<void> {
|
||||||
|
await api.respondToInvitation(projectID, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectInvitation(invite: ProjectInvitation): void {
|
||||||
|
state.selectedInvitation = invite;
|
||||||
|
}
|
||||||
|
|
||||||
function clear(): void {
|
function clear(): void {
|
||||||
state.projects = [];
|
state.projects = [];
|
||||||
state.selectedProject = defaultSelectedProject;
|
state.selectedProject = defaultSelectedProject;
|
||||||
@ -212,6 +233,8 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
state.settledBandwidthChartData = [];
|
state.settledBandwidthChartData = [];
|
||||||
state.chartDataSince = new Date();
|
state.chartDataSince = new Date();
|
||||||
state.chartDataBefore = new Date();
|
state.chartDataBefore = new Date();
|
||||||
|
state.invitations = [];
|
||||||
|
state.selectedInvitation = defaultSelectedInvitation;
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectsCount(userID: string): number {
|
function projectsCount(userID: string): number {
|
||||||
@ -257,6 +280,9 @@ export const useProjectsStore = defineStore('projects', () => {
|
|||||||
getProjectLimits,
|
getProjectLimits,
|
||||||
getTotalLimits,
|
getTotalLimits,
|
||||||
getProjectSalt,
|
getProjectSalt,
|
||||||
|
getUserInvitations,
|
||||||
|
respondToInvitation,
|
||||||
|
selectInvitation,
|
||||||
projectsCount,
|
projectsCount,
|
||||||
clear,
|
clear,
|
||||||
projects,
|
projects,
|
||||||
|
@ -43,7 +43,7 @@ export interface ProjectsApi {
|
|||||||
* Get project limits.
|
* Get project limits.
|
||||||
*
|
*
|
||||||
* @param projectId- project ID
|
* @param projectId- project ID
|
||||||
* throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
getLimits(projectId: string): Promise<ProjectLimits>;
|
getLimits(projectId: string): Promise<ProjectLimits>;
|
||||||
|
|
||||||
@ -51,21 +51,21 @@ export interface ProjectsApi {
|
|||||||
* Get project salt
|
* Get project salt
|
||||||
*
|
*
|
||||||
* @param projectID - project ID
|
* @param projectID - project ID
|
||||||
* throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
getSalt(projectID: string): Promise<string>;
|
getSalt(projectID: string): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get project limits.
|
* Get project limits.
|
||||||
*
|
*
|
||||||
* throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
getTotalLimits(): Promise<ProjectLimits>;
|
getTotalLimits(): Promise<ProjectLimits>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get project daily usage by specific date range.
|
* Get project daily usage by specific date range.
|
||||||
*
|
*
|
||||||
* throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
getDailyUsage(projectID: string, start: Date, end: Date): Promise<ProjectsStorageBandwidthDaily>;
|
getDailyUsage(projectID: string, start: Date, end: Date): Promise<ProjectsStorageBandwidthDaily>;
|
||||||
|
|
||||||
@ -76,6 +76,20 @@ export interface ProjectsApi {
|
|||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
getOwnedProjects(cursor: ProjectsCursor): Promise<ProjectsPage>;
|
getOwnedProjects(cursor: ProjectsCursor): Promise<ProjectsPage>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a user's pending project member invitations.
|
||||||
|
*
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
getUserInvitations(): Promise<ProjectInvitation[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles accepting or declining a user's project member invitation.
|
||||||
|
*
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
respondToInvitation(projectID: string, response: ProjectInvitationResponse): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -223,6 +237,27 @@ export class ProjectsStorageBandwidthDaily {
|
|||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProjectInvitation represents a pending project member invitation.
|
||||||
|
*/
|
||||||
|
export class ProjectInvitation {
|
||||||
|
public constructor(
|
||||||
|
public projectID: string,
|
||||||
|
public projectName: string,
|
||||||
|
public projectDescription: string,
|
||||||
|
public inviterEmail: string,
|
||||||
|
public createdAt: Date,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProjectInvitationResponse represents a response to a project member invitation.
|
||||||
|
*/
|
||||||
|
export enum ProjectInvitationResponse {
|
||||||
|
Decline,
|
||||||
|
Accept,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ProjectUsageDateRange is used to describe project's usage by date range.
|
* ProjectUsageDateRange is used to describe project's usage by date range.
|
||||||
*/
|
*/
|
||||||
|
@ -36,6 +36,7 @@ import EditSessionTimeoutModal from '@/components/modals/EditSessionTimeoutModal
|
|||||||
import UpgradeAccountModal from '@/components/modals/upgradeAccountFlow/UpgradeAccountModal.vue';
|
import UpgradeAccountModal from '@/components/modals/upgradeAccountFlow/UpgradeAccountModal.vue';
|
||||||
import DeleteAccessGrantModal from '@/components/modals/DeleteAccessGrantModal.vue';
|
import DeleteAccessGrantModal from '@/components/modals/DeleteAccessGrantModal.vue';
|
||||||
import SkipPassphraseModal from '@/components/modals/SkipPassphraseModal.vue';
|
import SkipPassphraseModal from '@/components/modals/SkipPassphraseModal.vue';
|
||||||
|
import JoinProjectModal from '@/components/modals/JoinProjectModal.vue';
|
||||||
|
|
||||||
export const APP_STATE_DROPDOWNS = {
|
export const APP_STATE_DROPDOWNS = {
|
||||||
ACCOUNT: 'isAccountDropdownShown',
|
ACCOUNT: 'isAccountDropdownShown',
|
||||||
@ -86,6 +87,7 @@ enum Modals {
|
|||||||
DELETE_ACCESS_GRANT = 'deleteAccessGrant',
|
DELETE_ACCESS_GRANT = 'deleteAccessGrant',
|
||||||
SKIP_PASSPHRASE = 'skipPassphrase',
|
SKIP_PASSPHRASE = 'skipPassphrase',
|
||||||
CHANGE_PROJECT_LIMIT = 'changeProjectLimit',
|
CHANGE_PROJECT_LIMIT = 'changeProjectLimit',
|
||||||
|
JOIN_PROJECT = 'joinProject',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MODALS: Record<Modals, Component> = {
|
export const MODALS: Record<Modals, Component> = {
|
||||||
@ -119,4 +121,5 @@ export const MODALS: Record<Modals, Component> = {
|
|||||||
[Modals.DELETE_ACCESS_GRANT]: DeleteAccessGrantModal,
|
[Modals.DELETE_ACCESS_GRANT]: DeleteAccessGrantModal,
|
||||||
[Modals.SKIP_PASSPHRASE]: SkipPassphraseModal,
|
[Modals.SKIP_PASSPHRASE]: SkipPassphraseModal,
|
||||||
[Modals.CHANGE_PROJECT_LIMIT]: ChangeProjectLimitModal,
|
[Modals.CHANGE_PROJECT_LIMIT]: ChangeProjectLimitModal,
|
||||||
|
[Modals.JOIN_PROJECT]: JoinProjectModal,
|
||||||
};
|
};
|
||||||
|
@ -42,9 +42,7 @@
|
|||||||
border-radius="8px"
|
border-radius="8px"
|
||||||
:is-disabled="isLoading"
|
:is-disabled="isLoading"
|
||||||
:on-press="onActivateClick"
|
:on-press="onActivateClick"
|
||||||
>
|
/>
|
||||||
Reset Password
|
|
||||||
</v-button>
|
|
||||||
<div class="activate-area__content-area__container__login-row">
|
<div class="activate-area__content-area__container__login-row">
|
||||||
<router-link :to="loginPath" class="activate-area__content-area__container__login-row__link">
|
<router-link :to="loginPath" class="activate-area__content-area__container__login-row__link">
|
||||||
Back to Login
|
Back to Login
|
||||||
|
@ -73,9 +73,7 @@
|
|||||||
border-radius="8px"
|
border-radius="8px"
|
||||||
:is-disabled="isLoading"
|
:is-disabled="isLoading"
|
||||||
:on-press="onSendConfigurations"
|
:on-press="onSendConfigurations"
|
||||||
>
|
/>
|
||||||
Reset Password
|
|
||||||
</v-button>
|
|
||||||
<div class="forgot-area__content-area__container__login-container">
|
<div class="forgot-area__content-area__container__login-container">
|
||||||
<router-link :to="loginPath" class="forgot-area__content-area__container__login-container__link">
|
<router-link :to="loginPath" class="forgot-area__content-area__container__login-container__link">
|
||||||
Back to Login
|
Back to Login
|
||||||
|
@ -123,9 +123,7 @@
|
|||||||
border-radius="50px"
|
border-radius="50px"
|
||||||
:is-disabled="isLoading"
|
:is-disabled="isLoading"
|
||||||
:on-press="onLoginClick"
|
:on-press="onLoginClick"
|
||||||
>
|
/>
|
||||||
Sign In
|
|
||||||
</v-button>
|
|
||||||
<span v-if="isMFARequired" class="login-area__content-area__container__cancel" :class="{ disabled: isLoading }" @click.prevent="onMFACancelClick">
|
<span v-if="isMFARequired" class="login-area__content-area__container__cancel" :class="{ disabled: isLoading }" @click.prevent="onMFACancelClick">
|
||||||
Cancel
|
Cancel
|
||||||
</span>
|
</span>
|
||||||
|
@ -661,6 +661,12 @@ onMounted(async () => {
|
|||||||
notify.error(`Unable to get credit cards. ${error.message}`, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
|
notify.error(`Unable to get credit cards. ${error.message}`, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await projectsStore.getUserInvitations();
|
||||||
|
} catch (error) {
|
||||||
|
notify.error(`Unable to get project invitations. ${error.message}`, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await projectsStore.getProjects();
|
await projectsStore.getProjects();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<div class="empty-project-item">
|
<div class="empty-project-item">
|
||||||
<div class="empty-project-item__header">
|
<div class="empty-project-item__header">
|
||||||
<div class="empty-project-item__header__tag">
|
<div class="empty-project-item__header__tag">
|
||||||
<box-icon />
|
<box-icon class="empty-project-item__header__tag__icon" />
|
||||||
|
|
||||||
<span> Project </span>
|
<span> Project </span>
|
||||||
</div>
|
</div>
|
||||||
@ -40,7 +40,7 @@ import { useProjectsStore } from '@/store/modules/projectsStore';
|
|||||||
|
|
||||||
import VButton from '@/components/common/VButton.vue';
|
import VButton from '@/components/common/VButton.vue';
|
||||||
|
|
||||||
import BoxIcon from '@/../static/images/allDashboard/box.svg';
|
import BoxIcon from '@/../static/images/navigation/project.svg';
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
@ -91,16 +91,20 @@ function onCreateProjectClicked(): void {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-family: 'font_regular', sans-serif;
|
font-family: 'font_regular', sans-serif;
|
||||||
|
|
||||||
& :deep(svg path) {
|
&__icon {
|
||||||
fill: var(--c-blue-4);
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
|
||||||
|
:deep(path) {
|
||||||
|
fill: var(--c-blue-4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
font-weight: bold;
|
font-family: 'font_bold', sans-serif;
|
||||||
font-family: 'font_regular', sans-serif;
|
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
line-height: 31px;
|
line-height: 31px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -15,8 +15,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="projects.length" class="my-projects__list">
|
<div v-if="projects.length || invites.length" class="my-projects__list">
|
||||||
<project-item v-for="project in projects" :key="project.id" :project="project" />
|
<project-item v-for="project in projects" :key="project.id" :project="project" />
|
||||||
|
<project-invitation-item v-for="invite in invites" :key="invite.projectID" :invitation="invite" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="my-projects__empty-area">
|
<div v-else class="my-projects__empty-area">
|
||||||
<empty-project-item class="my-projects__empty-area__item" />
|
<empty-project-item class="my-projects__empty-area__item" />
|
||||||
@ -28,7 +29,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { Project } from '@/types/projects';
|
import { Project, ProjectInvitation } from '@/types/projects';
|
||||||
import { RouteConfig } from '@/router';
|
import { RouteConfig } from '@/router';
|
||||||
import {
|
import {
|
||||||
AnalyticsEvent,
|
AnalyticsEvent,
|
||||||
@ -38,6 +39,7 @@ import { MODALS } from '@/utils/constants/appStatePopUps';
|
|||||||
import { AnalyticsHttpApi } from '@/api/analytics';
|
import { AnalyticsHttpApi } from '@/api/analytics';
|
||||||
import EmptyProjectItem from '@/views/all-dashboard/components/EmptyProjectItem.vue';
|
import EmptyProjectItem from '@/views/all-dashboard/components/EmptyProjectItem.vue';
|
||||||
import ProjectItem from '@/views/all-dashboard/components/ProjectItem.vue';
|
import ProjectItem from '@/views/all-dashboard/components/ProjectItem.vue';
|
||||||
|
import ProjectInvitationItem from '@/views/all-dashboard/components/ProjectInvitationItem.vue';
|
||||||
import { useUsersStore } from '@/store/modules/usersStore';
|
import { useUsersStore } from '@/store/modules/usersStore';
|
||||||
import { useAppStore } from '@/store/modules/appStore';
|
import { useAppStore } from '@/store/modules/appStore';
|
||||||
import { useProjectsStore } from '@/store/modules/projectsStore';
|
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||||
@ -59,6 +61,14 @@ const projects = computed((): Project[] => {
|
|||||||
return projectsStore.projects;
|
return projectsStore.projects;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns project member invitations list from store.
|
||||||
|
*/
|
||||||
|
const invites = computed((): ProjectInvitation[] => {
|
||||||
|
return projectsStore.state.invitations.slice()
|
||||||
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route to create project page.
|
* Route to create project page.
|
||||||
*/
|
*/
|
||||||
@ -111,18 +121,18 @@ function onCreateProjectClicked(): void {
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
|
||||||
& :deep(.project-item) {
|
& :deep(.project-item) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (width <= 1024px) {
|
@media screen and (width <= 1024px) {
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (width <= 786px) {
|
@media screen and (width <= 786px) {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (width <= 425px) {
|
@media screen and (width <= 425px) {
|
||||||
|
@ -0,0 +1,163 @@
|
|||||||
|
// Copyright (C) 2023 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="invite-item">
|
||||||
|
<project-ownership-tag class="invite-item__tag" :is-invited="true" />
|
||||||
|
|
||||||
|
<div class="invite-item__info">
|
||||||
|
<p class="invite-item__info__name">{{ invitation.projectName }}</p>
|
||||||
|
<p class="invite-item__info__description">{{ invitation.projectDescription }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="invite-item__buttons">
|
||||||
|
<VButton
|
||||||
|
class="invite-item__buttons__button"
|
||||||
|
width="fit-content"
|
||||||
|
border-radius="8px"
|
||||||
|
font-size="12px"
|
||||||
|
:is-disabled="isLoading"
|
||||||
|
:on-press="onJoinClicked"
|
||||||
|
>
|
||||||
|
<span class="invite-item__buttons__button__join-label">Join Project</span>
|
||||||
|
<span class="invite-item__buttons__button__join-label-mobile">Join</span>
|
||||||
|
</VButton>
|
||||||
|
<VButton
|
||||||
|
class="invite-item__buttons__button decline"
|
||||||
|
border-radius="8px"
|
||||||
|
font-size="12px"
|
||||||
|
:is-disabled="isLoading"
|
||||||
|
:is-transparent="true"
|
||||||
|
:on-press="onDeclineClicked"
|
||||||
|
label="Decline"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { ProjectInvitation, ProjectInvitationResponse } from '@/types/projects';
|
||||||
|
import { useNotify } from '@/utils/hooks';
|
||||||
|
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
|
||||||
|
import { MODALS } from '@/utils/constants/appStatePopUps';
|
||||||
|
import { useAppStore } from '@/store/modules/appStore';
|
||||||
|
import { useProjectsStore } from '@/store/modules/projectsStore';
|
||||||
|
|
||||||
|
import VButton from '@/components/common/VButton.vue';
|
||||||
|
import ProjectOwnershipTag from '@/components/project/ProjectOwnershipTag.vue';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const projectsStore = useProjectsStore();
|
||||||
|
const notify = useNotify();
|
||||||
|
|
||||||
|
const isLoading = ref<boolean>(false);
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
invitation?: ProjectInvitation,
|
||||||
|
}>(), {
|
||||||
|
invitation: () => new ProjectInvitation('', '', '', '', new Date()),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the Join Project modal.
|
||||||
|
*/
|
||||||
|
function onJoinClicked(): void {
|
||||||
|
projectsStore.selectInvitation(props.invitation);
|
||||||
|
appStore.updateActiveModal(MODALS.joinProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declines the project member invitation.
|
||||||
|
*/
|
||||||
|
async function onDeclineClicked(): Promise<void> {
|
||||||
|
if (isLoading.value) return;
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await projectsStore.respondToInvitation(props.invitation.projectID, ProjectInvitationResponse.Decline);
|
||||||
|
} catch (error) {
|
||||||
|
notify.error(`Failed to decline project invitation. ${error.message}`, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await projectsStore.getUserInvitations();
|
||||||
|
await projectsStore.getProjects();
|
||||||
|
} catch (error) {
|
||||||
|
notify.error(`Failed to reload projects and invitations list. ${error.message}`, AnalyticsErrorEventSource.ALL_PROJECT_DASHBOARD);
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.invite-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--c-white);
|
||||||
|
box-shadow: 0 0 20px rgb(0 0 0 / 5%);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&__tag {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
font-family: 'font_bold', sans-serif;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 31px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
font-family: 'font_regular', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
min-height: 20px;
|
||||||
|
color: var(--c-grey-6);
|
||||||
|
line-height: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
line-height: 20px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
|
||||||
|
&__join-label-mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (width <= 520px) and (width > 425px) {
|
||||||
|
|
||||||
|
&__join-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__join-label-mobile {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -26,19 +26,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="project-item__name">
|
<div class="project-item__info">
|
||||||
{{ project.name }}
|
<p class="project-item__info__name">{{ project.name }}</p>
|
||||||
</p>
|
<p class="project-item__info__description">{{ project.description }}</p>
|
||||||
|
</div>
|
||||||
<p class="project-item__description">
|
|
||||||
{{ project.description }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<VButton
|
<VButton
|
||||||
class="project-item__button"
|
class="project-item__button"
|
||||||
width="fit-content"
|
width="fit-content"
|
||||||
height="fit-content"
|
|
||||||
border-radius="8px"
|
border-radius="8px"
|
||||||
|
font-size="12px"
|
||||||
:on-press="onOpenClicked"
|
:on-press="onOpenClicked"
|
||||||
label="Open Project"
|
label="Open Project"
|
||||||
/>
|
/>
|
||||||
@ -50,7 +47,6 @@ import { computed } from 'vue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Project } from '@/types/projects';
|
import { Project } from '@/types/projects';
|
||||||
import { useNotify } from '@/utils/hooks';
|
|
||||||
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
|
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
|
||||||
import { User } from '@/types/users';
|
import { User } from '@/types/users';
|
||||||
import { AnalyticsHttpApi } from '@/api/analytics';
|
import { AnalyticsHttpApi } from '@/api/analytics';
|
||||||
@ -75,7 +71,6 @@ const appStore = useAppStore();
|
|||||||
const pmStore = useProjectMembersStore();
|
const pmStore = useProjectMembersStore();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
const notify = useNotify();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const analytics = new AnalyticsHttpApi();
|
const analytics = new AnalyticsHttpApi();
|
||||||
@ -165,11 +160,11 @@ async function goToProjectEdit(): Promise<void> {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.project-item {
|
.project-item {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-rows: 1fr 1fr 1fr 1fr;
|
align-items: stretch;
|
||||||
align-items: start;
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
height: 200px;
|
|
||||||
background: var(--c-white);
|
background: var(--c-white);
|
||||||
box-shadow: 0 0 20px rgb(0 0 0 / 5%);
|
box-shadow: 0 0 20px rgb(0 0 0 / 5%);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -231,30 +226,36 @@ async function goToProjectEdit(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__name {
|
&__info {
|
||||||
font-family: 'font_bold', sans-serif;
|
display: flex;
|
||||||
font-size: 24px;
|
gap: 4px;
|
||||||
line-height: 31px;
|
flex-direction: column;
|
||||||
width: 100%;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__description {
|
&__name {
|
||||||
font-family: 'font_regular', sans-serif;
|
font-family: 'font_bold', sans-serif;
|
||||||
font-size: 14px;
|
font-size: 24px;
|
||||||
color: var(--c-grey-6);
|
line-height: 31px;
|
||||||
line-height: 20px;
|
white-space: nowrap;
|
||||||
width: 100%;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-align: start;
|
||||||
overflow: hidden;
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
font-family: 'font_regular', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
min-height: 20px;
|
||||||
|
color: var(--c-grey-6);
|
||||||
|
line-height: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__button {
|
&__button {
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -257,9 +257,7 @@
|
|||||||
border-radius="50px"
|
border-radius="50px"
|
||||||
:is-disabled="isLoading"
|
:is-disabled="isLoading"
|
||||||
:on-press="onCreateClick"
|
:on-press="onCreateClick"
|
||||||
>
|
/>
|
||||||
Sign In
|
|
||||||
</v-button>
|
|
||||||
<div class="register-area__input-area__login-container">
|
<div class="register-area__input-area__login-container">
|
||||||
Already have an account? <router-link :to="loginPath" class="register-area__input-area__login-container__link">Login.</router-link>
|
Already have an account? <router-link :to="loginPath" class="register-area__input-area__login-container__link">Login.</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M5.13404 0.652105L5.15302 0.662491L9.44915 3.14357C9.59279 3.22653 9.66993 3.36706 9.68057 3.51186L9.68147 3.52864L9.68184 3.54736V8.46475C9.68184 8.62442 9.60006 8.77242 9.46612 8.85756L9.44836 8.86831L5.1522 11.3382C5.01422 11.4175 4.84577 11.4206 4.70545 11.3476L4.68649 11.3372L0.43205 8.86732C0.294547 8.7875 0.20772 8.64332 0.200723 8.48542L0.200195 8.46475L0.20035 3.53861L0.200195 3.52439C0.20104 3.37069 0.278044 3.22133 0.415856 3.13378L0.432684 3.12357L4.68716 0.66265C4.81842 0.586724 4.97752 0.579914 5.11378 0.642238L5.13404 0.652105ZM8.75081 4.35305L5.38566 6.29643V10.13L8.75081 8.19537V4.35305ZM1.13125 4.35694V8.19673L4.45468 10.1261V6.29507L1.13125 4.35694ZM4.92034 1.60319L1.57515 3.5381L4.92135 5.48952L8.28549 3.54665L4.92034 1.60319Z" fill="#7B61FF"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 887 B |
12
web/satellite/static/images/modals/boxesIcon.svg
Normal file
12
web/satellite/static/images/modals/boxesIcon.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4423 0H23.3463C28.8835 0 31.0805 0.613722 33.2353 1.76616C35.3902 2.91861 37.0814 4.60977 38.2338 6.76465L38.3214 6.93055C39.4029 9.0067 39.9846 11.2 40 16.4423V23.3463C40 28.8835 39.3863 31.0804 38.2338 33.2353C37.0814 35.3902 35.3902 37.0813 33.2353 38.2338L33.0694 38.3214C30.9933 39.4029 28.8 39.9845 23.5577 39.9999H16.6537C11.1165 39.9999 8.91954 39.3862 6.76466 38.2338C4.60977 37.0813 2.91861 35.3902 1.76617 33.2353L1.67858 33.0694C0.597074 30.9932 0.0154219 28.8 0 23.5576V16.6536C0 11.1165 0.613723 8.91953 1.76617 6.76465C2.91861 4.60977 4.60977 2.91861 6.76466 1.76616L6.93055 1.67858C9.00672 0.597073 11.2 0.0154218 16.4423 0Z" fill="#D8DEE3"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.111086 13.1133L6.44892 16.7727V29.5423L0.0504854 25.8484C0.0193827 25.1545 0.00246402 24.3947 0 23.5571V16.6532C0 15.2862 0.037405 14.1227 0.111086 13.1133Z" fill="#FF458B"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.4491 4.09766L17.508 10.4825L6.4491 16.8672L0.104614 13.2042C0.322552 10.1004 0.877608 8.43954 1.73786 6.81787L6.4491 4.09766Z" fill="#FFA3C5"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.0001 16.4419V23.3459C40.0001 24.3675 39.9792 25.2753 39.9379 26.0902L34.1838 29.4118V16.6422L39.9048 13.3389C39.9646 14.2416 39.9966 15.2649 40.0001 16.4419Z" fill="#FF458B"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.2791 3.9668L37.784 5.99021C37.942 6.24119 38.092 6.49934 38.2338 6.76451L38.3214 6.93041C39.1729 8.56503 39.7145 10.2722 39.9139 13.4831L34.2791 16.7364L23.2203 10.3516L34.2791 3.9668Z" fill="#FFA3C5"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.2789 16.6426L34.2789 29.4122L23.2201 23.0274L23.2201 10.2578L34.2789 16.6426Z" fill="#DC0D5B"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.35376 16.7735L6.35376 29.5431L17.4126 23.1583L17.4126 10.3887L6.35376 16.7735Z" fill="#DC0D5B"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.2509 12.0664L31.3097 18.4512L20.2509 24.8359L9.19202 18.4512L20.2509 12.0664Z" fill="#87A9FF"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.2506 24.7412L20.2506 37.5108L9.1919 31.126L9.19189 18.3564L20.2506 24.7412Z" fill="#0218A7"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1556 24.7412L20.1556 37.5108L31.2144 31.126L31.2144 18.3564L20.1556 24.7412Z" fill="#0149FF"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
@ -4,6 +4,8 @@
|
|||||||
import {
|
import {
|
||||||
Project,
|
Project,
|
||||||
ProjectFields,
|
ProjectFields,
|
||||||
|
ProjectInvitation,
|
||||||
|
ProjectInvitationResponse,
|
||||||
ProjectLimits,
|
ProjectLimits,
|
||||||
ProjectsApi,
|
ProjectsApi,
|
||||||
ProjectsCursor,
|
ProjectsCursor,
|
||||||
@ -62,4 +64,12 @@ export class ProjectsApiMock implements ProjectsApi {
|
|||||||
getDailyUsage(_projectId: string, _start: Date, _end: Date): Promise<ProjectsStorageBandwidthDaily> {
|
getDailyUsage(_projectId: string, _start: Date, _end: Date): Promise<ProjectsStorageBandwidthDaily> {
|
||||||
throw new Error('not implemented');
|
throw new Error('not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserInvitations(): Promise<ProjectInvitation[]> {
|
||||||
|
throw new Error('not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
respondToInvitation(_projectID: string, _response: ProjectInvitationResponse): Promise<void> {
|
||||||
|
throw new Error('not implemented');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user