web/satellite: onboarding tour: api keys and upload data steps

Change-Id: I8ffa6d688a22c1568495a7e0e176096cadcd6eaa
This commit is contained in:
VitaliiShpital 2020-04-30 23:35:09 +03:00 committed by Vitalii Shpital
parent 7a83473f00
commit 2284008b8c
29 changed files with 1105 additions and 72 deletions

View File

@ -61,7 +61,6 @@ import { RouteConfig } from '@/router';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { DateRange } from '@/types/payments';
import { Project } from '@/types/projects';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { SegmentEvent } from '@/utils/constants/analyticsEventNames';
import { ProjectOwning } from '@/utils/projectOwning';
@ -95,7 +94,7 @@ export default class BillingArea extends Vue {
* Fetches billing history and project limits.
*/
public async beforeMount(): Promise<void> {
if (!this.$store.getters.selectedProject.id) {
if (this.noProjectOrApiKeys) {
await this.$router.push(RouteConfig.OnboardingTour.path);
return;
@ -284,6 +283,13 @@ export default class BillingArea extends Vue {
}
}
/**
* Indicates if user has no project nor api keys.
*/
private get noProjectOrApiKeys(): boolean {
return !this.$store.getters.selectedProject.id || this.$store.state.apiKeysModule.page.apiKeys.length === 0;
}
/**
* Changes buttons styling depends on selected status.
* @param event holds click event

View File

@ -3,7 +3,7 @@
<template>
<div class="api-keys-area">
<h1 class="api-keys-area__title" v-if="isTitleShown">API Keys</h1>
<h1 class="api-keys-area__title" v-if="!isEmpty">API Keys</h1>
<div class="api-keys-area__container">
<ApiKeysCreationPopup
@closePopup="closeNewApiKeyPopup"
@ -93,7 +93,6 @@
<EmptySearchResultIcon class="empty-search-result-area__image"/>
</div>
<NoApiKeysArea
:class="{ collapsed: isBannerShown }"
:on-button-click="onCreateApiKeyClick"
v-if="isEmptyStateShown"
/>
@ -249,7 +248,7 @@ export default class ApiKeysArea extends Vue {
try {
await this.$store.dispatch(FETCH, this.FIRST_PAGE);
} catch (error) {
await this.notifyFetchError(error);
await this.$notify.error(`Unable to fetch API keys. ${error.message}`);
}
this.isDeleteClicked = false;
@ -291,20 +290,6 @@ export default class ApiKeysArea extends Vue {
return this.$store.getters.apiKeys.length === 0;
}
/**
* Indicates if bonus banner should appear if no credit cards is attached to account.
*/
public get isBannerShown(): boolean {
return this.$store.state.paymentsModule.creditCards.length === 0;
}
/**
* Indicates if "Account" title is shown.
*/
public get isTitleShown(): boolean {
return !(this.isBannerShown && this.isEmpty);
}
public get hasSearchQuery(): boolean {
return this.$store.state.apiKeysModule.cursor.search;
}
@ -349,7 +334,7 @@ export default class ApiKeysArea extends Vue {
try {
await this.$store.dispatch(FETCH, index);
} catch (error) {
await this.notifyFetchError(error);
await this.$notify.error(`Unable to fetch API keys. ${error.message}`);
}
}
@ -364,7 +349,7 @@ export default class ApiKeysArea extends Vue {
try {
await this.$store.dispatch(FETCH, this.FIRST_PAGE);
} catch (error) {
await this.notifyFetchError(error);
await this.$notify.error(`Unable to fetch API keys. ${error.message}`);
}
if (this.totalPageCount > 1) {
@ -381,21 +366,13 @@ export default class ApiKeysArea extends Vue {
try {
await this.$store.dispatch(FETCH, this.FIRST_PAGE);
} catch (error) {
await this.notifyFetchError(error);
await this.$notify.error(`Unable to fetch API keys. ${error.message}`);
}
if (this.totalPageCount > 1) {
this.$refs.pagination.resetPageIndex();
}
}
/**
* Fires UI notification with message.
* @param error
*/
public async notifyFetchError(error: Error): Promise<void> {
await this.$notify.error(`Unable to fetch API keys. ${error.message}`);
}
}
</script>

View File

@ -66,7 +66,7 @@ export default class VButton extends Vue {
}
.blue-white {
background-color: transparent !important;
background-color: #fff !important;
border: 2px solid #2683ff !important;
.label {

View File

@ -34,11 +34,11 @@ import { ProjectOwning } from '@/utils/projectOwning';
export default class NewProjectArea extends Vue {
// TODO: temporary solution. Remove when user will be able to create more then one project
/**
* Life cycle hook after initial render.
* Life cycle hook before initial render.
* Toggles new project button visibility depending on user having his own project or payment method.
*/
public beforeMount(): void {
if (this.userHasOwnProject || !this.$store.getters.canUserCreateFirstProject || this.isOnboardingTour) {
if (this.userHasOwnProject || !this.$store.getters.canUserCreateFirstProject) {
this.$store.dispatch(APP_STATE_ACTIONS.HIDE_CREATE_PROJECT_BUTTON);
return;
@ -47,6 +47,16 @@ export default class NewProjectArea extends Vue {
this.$store.dispatch(APP_STATE_ACTIONS.SHOW_CREATE_PROJECT_BUTTON);
}
/**
* Life cycle hook after initial render.
* Hides new project button visibility if user is on onboarding tour.
*/
public mounted(): void {
if (this.isOnboardingTour) {
this.$store.dispatch(APP_STATE_ACTIONS.HIDE_CREATE_PROJECT_BUTTON);
}
}
/**
* Opens new project creation popup.
*/

View File

@ -4,10 +4,15 @@
<template>
<div class="project-selection-container" id="projectDropdownButton">
<p class="project-selection-container__no-projects-text" v-if="!hasProjects">You have no projects</p>
<div class="project-selection-toggle-container" @click="toggleSelection" v-if="hasProjects">
<p class="project-selection-toggle-container__common">Project:</p>
<div
class="project-selection-toggle-container"
:class="{ default: isOnboardingTour }"
@click="toggleSelection"
v-if="hasProjects"
>
<p class="project-selection-toggle-container__common" :class="{ default: isOnboardingTour }">Project:</p>
<h1 class="project-selection-toggle-container__name">{{name}}</h1>
<div class="project-selection-toggle-container__expander-area">
<div class="project-selection-toggle-container__expander-area" v-if="!isOnboardingTour">
<ExpandIcon
v-if="!isDropdownShown"
alt="Arrow down (expand)"
@ -28,6 +33,7 @@ import { Component, Vue } from 'vue-property-decorator';
import ExpandIcon from '@/../static/images/common/BlueExpand.svg';
import HideIcon from '@/../static/images/common/BlueHide.svg';
import { RouteConfig } from '@/router';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { Project } from '@/types/projects';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
@ -48,7 +54,7 @@ export default class ProjectSelectionArea extends Vue {
* Fetches projects related information and than toggles selection popup.
*/
public async toggleSelection(): Promise<void> {
if (this.isLoading) return;
if (this.isLoading || this.isOnboardingTour) return;
this.isLoading = true;
@ -86,6 +92,13 @@ export default class ProjectSelectionArea extends Vue {
public get hasProjects(): boolean {
return !!this.$store.state.projectsModule.projects.length;
}
/**
* Indicates if current route is onboarding tour.
*/
public get isOnboardingTour(): boolean {
return this.$route.name === RouteConfig.OnboardingTour.name;
}
}
</script>
@ -141,6 +154,10 @@ export default class ProjectSelectionArea extends Vue {
}
}
.default {
cursor: default;
}
@media screen and (max-width: 1280px) {
.project-selection-container {

View File

@ -5,6 +5,8 @@
<div class="tour-area">
<ProgressBar
:is-create-project-step="isCreateProjectState"
:is-create-api-key-step="isCreatApiKeyState"
:is-upload-data-step="isUploadDataState"
/>
<OverviewStep
v-if="isDefaultState"
@ -16,7 +18,13 @@
/>
<CreateProjectStep
v-if="isCreateProjectState"
@setApiKeyState="setCreateApiKeyState"
/>
<CreateApiKeyStep
v-if="isCreatApiKeyState"
@setUploadDataState="setUploadDataState"
/>
<UploadDataStep v-if="isUploadDataState"/>
</div>
</template>
@ -25,8 +33,10 @@ import { Component, Vue } from 'vue-property-decorator';
import ProgressBar from '@/components/onboardingTour/ProgressBar.vue';
import AddPaymentStep from '@/components/onboardingTour/steps/AddPaymentStep.vue';
import CreateApiKeyStep from '@/components/onboardingTour/steps/CreateApiKeyStep.vue';
import CreateProjectStep from '@/components/onboardingTour/steps/CreateProjectStep.vue';
import OverviewStep from '@/components/onboardingTour/steps/OverviewStep.vue';
import UploadDataStep from '@/components/onboardingTour/steps/UploadDataStep.vue';
import CheckedImage from '@/../static/images/common/checked.svg';
@ -35,6 +45,8 @@ import { TourState } from '@/utils/constants/onboardingTourEnums';
@Component({
components: {
UploadDataStep,
CreateApiKeyStep,
CreateProjectStep,
AddPaymentStep,
ProgressBar,
@ -51,7 +63,7 @@ export default class OnboardingTourArea extends Vue {
* Sets area to needed state.
*/
public mounted(): void {
if (this.$store.state.projectsModule.projects.length > 0) {
if (this.userHasProject && this.userHasApiKeys) {
try {
this.$router.push(RouteConfig.ProjectDashboard.path);
} catch (error) {
@ -61,8 +73,16 @@ export default class OnboardingTourArea extends Vue {
return;
}
if (this.userHasProject && !this.userHasApiKeys) {
this.setCreateApiKeyState();
return;
}
if (this.$store.state.paymentsModule.creditCards.length > 0) {
this.setCreateProjectState();
return;
}
if (this.$store.getters.isTransactionProcessing || this.$store.getters.isTransactionCompleted) {
@ -91,6 +111,20 @@ export default class OnboardingTourArea extends Vue {
return this.areaState === TourState.PROJECT;
}
/**
* Indicates if area is in api key state.
*/
public get isCreatApiKeyState(): boolean {
return this.areaState === TourState.API_KEY;
}
/**
* Indicates if area is in upload data state.
*/
public get isUploadDataState(): boolean {
return this.areaState === TourState.UPLOAD;
}
/**
* Sets area's state to adding payment method state.
*/
@ -104,6 +138,34 @@ export default class OnboardingTourArea extends Vue {
public setCreateProjectState(): void {
this.areaState = TourState.PROJECT;
}
/**
* Sets area's state to creating api key state.
*/
public setCreateApiKeyState(): void {
this.areaState = TourState.API_KEY;
}
/**
* Sets area's state to upload data state.
*/
public setUploadDataState(): void {
this.areaState = TourState.UPLOAD;
}
/**
* Indicates if user has at least one project.
*/
private get userHasProject(): boolean {
return this.$store.state.projectsModule.projects.length > 0;
}
/**
* Indicates if user has at least one API key.
*/
private get userHasApiKeys(): boolean {
return this.$store.state.apiKeysModule.page.apiKeys.length > 0;
}
}
</script>

View File

@ -4,24 +4,52 @@
<template>
<div class="progress-bar-container">
<div class="progress-bar-container__progress-area">
<div class="progress-bar-container__progress-area__circle" :class="{ 'completed-step': isCreateProjectStep }">
<div
class="progress-bar-container__progress-area__circle"
:class="{ 'completed-step': isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep }"
>
<CheckedImage/>
</div>
<div class="progress-bar-container__progress-area__bar"/>
<div class="progress-bar-container__progress-area__circle">
<div
class="progress-bar-container__progress-area__bar"
:class="{ 'completed-step': isCreateApiKeyStep || isUploadDataStep }"
/>
<div
class="progress-bar-container__progress-area__circle"
:class="{ 'completed-step': isCreateApiKeyStep || isUploadDataStep }"
>
<CheckedImage/>
</div>
<div class="progress-bar-container__progress-area__bar"/>
<div class="progress-bar-container__progress-area__circle">
<div
class="progress-bar-container__progress-area__bar"
:class="{ 'completed-step': isUploadDataStep }"
/>
<div
class="progress-bar-container__progress-area__circle"
:class="{ 'completed-step': isUploadDataStep }"
>
<CheckedImage/>
</div>
</div>
<div class="progress-bar-container__titles-area">
<span class="progress-bar-container__titles-area__title" :class="{ 'completed-font-color': isCreateProjectStep }">
<span
class="progress-bar-container__titles-area__title"
:class="{ 'completed-font-color': isCreateProjectStep || isCreateApiKeyStep || isUploadDataStep }"
>
Name Your Project
</span>
<span class="progress-bar-container__titles-area__title api-key-title">Create an API Key</span>
<span class="progress-bar-container__titles-area__title">Upload Data</span>
<span
class="progress-bar-container__titles-area__title api-key-title"
:class="{ 'completed-font-color': isCreateApiKeyStep || isUploadDataStep }"
>
Create an API Key
</span>
<span
class="progress-bar-container__titles-area__title"
:class="{ 'completed-font-color': isUploadDataStep }"
>
Upload Data
</span>
</div>
</div>
</template>
@ -40,6 +68,10 @@ import CheckedImage from '@/../static/images/common/checked.svg';
export default class ProgressBar extends Vue {
@Prop({ default: false })
public readonly isCreateProjectStep: boolean;
@Prop({ default: false })
public readonly isCreateApiKeyStep: boolean;
@Prop({ default: false })
public readonly isUploadDataStep: boolean;
}
</script>

View File

@ -0,0 +1,362 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="create-api-key-step">
<h1 class="create-api-key-step__title">Create an API Key</h1>
<p class="create-api-key-step__sub-title">
API keys provide access to the project for creating buckets and uploading objects through the command line
interface. This will be your first API key, and you can always create more keys later on.
</p>
<div class="create-api-key-step__container">
<div class="create-api-key-step__container__title-area">
<h2 class="create-api-key-step__container__title-area__title">Create API Key</h2>
<img
v-if="isLoading"
class="create-api-key-step__container__title-area__loading-image"
src="@/../static/images/account/billing/loading.gif"
alt="loading gif"
>
</div>
<HeaderedInput
label="API Key Name"
placeholder="Enter API Key Name (i.e. Dans Key)"
class="full-input"
width="calc(100% - 4px)"
:error="errorMessage"
@setData="setApiKeyName"
/>
<div class="create-api-key-step__container__create-key-area" v-if="isCreatingState">
<VButton
class="generate-button"
width="100%"
height="40px"
label="Generate API Key"
:is-blue-white="true"
:on-press="createApiKey"
/>
</div>
<div class="create-api-key-step__container__copy-key-area" v-else>
<div class="create-api-key-step__container__copy-key-area__header">
<InfoImage/>
<span class="create-api-key-step__container__copy-key-area__header__title">
API Keys only appear here once. Copy and paste this key to your preferred method of storing secrets.
</span>
</div>
<div class="create-api-key-step__container__copy-key-area__key-container">
<span class="create-api-key-step__container__copy-key-area__key-container__key">{{ key }}</span>
<div class="create-api-key-step__container__copy-key-area__key-container__copy-button">
<VButton
width="81px"
height="40px"
label="Copy"
:is-blue-white="true"
:on-press="onCopyClick"
/>
</div>
</div>
</div>
<p class="create-api-key-step__container__info" v-if="isCopyState">
We dont record your API Keys, which are only displayed once when generated. If you loose this
key, it cannot be recovered but you can always create new API Keys when needed.
</p>
<div class="create-api-key-step__container__blur" v-if="isLoading"/>
</div>
<VButton
class="done-button"
width="156px"
height="48px"
label="Done"
:on-press="onDoneClick"
:is-disabled="isCreatingState"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HeaderedInput from '@/components/common/HeaderedInput.vue';
import VButton from '@/components/common/VButton.vue';
import InfoImage from '@/../static/images/onboardingTour/info.svg';
import { ApiKey } from '@/types/apiKeys';
import { API_KEYS_ACTIONS } from '@/utils/constants/actionNames';
import { SegmentEvent } from '@/utils/constants/analyticsEventNames';
import { AddingApiKeyState } from '@/utils/constants/onboardingTourEnums';
const {
CREATE,
FETCH,
} = API_KEYS_ACTIONS;
@Component({
components: {
VButton,
HeaderedInput,
InfoImage,
},
})
export default class CreateApiKeyStep extends Vue {
private name: string = '';
private addingState: number = AddingApiKeyState.CREATE;
private readonly FIRST_PAGE = 1;
public key: string = '';
public errorMessage: string = '';
public isLoading: boolean = false;
/**
* Indicates if view is in creating state.
*/
public get isCreatingState(): boolean {
return this.addingState === AddingApiKeyState.CREATE;
}
/**
* Indicates if view is in copy state.
*/
public get isCopyState(): boolean {
return this.addingState === AddingApiKeyState.COPY;
}
/**
* Indicates view state to copy state.
*/
public setCopyState(): void {
this.addingState = AddingApiKeyState.COPY;
}
/**
* Sets api key name from input value.
*/
public setApiKeyName(value: string): void {
this.name = value.trim();
this.errorMessage = '';
}
/**
* Creates api key and refreshes store.
*/
public async createApiKey(): Promise<void> {
if (this.isLoading) {
return;
}
if (!this.name) {
this.errorMessage = 'API Key name can`t be empty';
return;
}
this.isLoading = true;
let createdApiKey: ApiKey;
try {
createdApiKey = await this.$store.dispatch(CREATE, this.name);
} catch (error) {
await this.$notify.error(error.message);
this.isLoading = false;
return;
}
await this.$notify.success('Successfully created new api key');
this.key = createdApiKey.secret;
this.$segment.track(SegmentEvent.API_KEY_CREATED, {
project_id: this.$store.getters.selectedProject.id,
});
try {
await this.$store.dispatch(FETCH, this.FIRST_PAGE);
} catch (error) {
await this.$notify.error(`Unable to fetch API keys. ${error.message}`);
}
this.setCopyState();
this.isLoading = false;
}
/**
* Copies api key secret to buffer.
*/
public onCopyClick(): void {
this.$copyText(this.key);
this.$notify.success('Key saved to clipboard');
}
/**
* Sets tour state to last step.
*/
public onDoneClick(): void {
this.$emit('setUploadDataState');
}
}
</script>
<style scoped lang="scss">
h1,
h2,
p {
margin: 0;
}
.create-api-key-step {
font-family: 'font_regular', sans-serif;
margin-top: 75px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 0 200px;
&__title {
font-size: 32px;
line-height: 39px;
color: #1b2533;
margin-bottom: 25px;
}
&__sub-title {
font-size: 16px;
line-height: 19px;
color: #354049;
margin-bottom: 35px;
text-align: center;
word-break: break-word;
}
&__container {
padding: 50px;
width: calc(100% - 100px);
border-radius: 8px;
background-color: #fff;
position: relative;
margin-bottom: 30px;
&__title-area {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: 10px;
&__title {
font-family: 'font_medium', sans-serif;
font-size: 22px;
line-height: 27px;
color: #354049;
margin-right: 15px;
}
&__loading-image {
width: 18px;
height: 18px;
}
}
&__create-key-area {
width: calc(100% - 90px);
padding: 40px 45px;
margin-top: 30px;
background-color: #0c2546;
border-radius: 8px;
}
&__copy-key-area {
width: 100%;
margin-top: 30px;
&__header {
padding: 10px;
width: calc(100% - 20px);
background-color: #ce3030;
display: flex;
align-items: center;
justify-content: flex-start;
border-radius: 8px 8px 0 0;
&__title {
font-size: 12px;
line-height: 16px;
color: #fff;
}
}
&__key-container {
background-color: #0c2546;
display: flex;
align-items: center;
padding: 20px 25px;
width: calc(100% - 50px);
border-radius: 0 0 8px 8px;
&__key {
font-size: 15px;
line-height: 23px;
color: #fff;
margin-right: 50px;
word-break: break-all;
}
&__copy-button {
min-width: 85px;
}
}
}
&__info {
width: 100%;
margin-top: 30px;
font-size: 12px;
line-height: 18px;
text-align: center;
color: #354049;
word-break: break-word;
}
&__blur {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: rgba(229, 229, 229, 0.2);
z-index: 100;
}
}
}
.full-input {
width: 100%;
}
.info-svg {
min-width: 18px;
margin-right: 5px;
}
@media screen and (max-width: 1650px) {
.create-api-key-step {
padding: 0 100px;
}
}
@media screen and (max-width: 1450px) {
.create-api-key-step {
padding: 0 60px;
}
}
@media screen and (max-width: 900px) {
.create-api-key-step {
padding: 0 50px;
}
}
</style>

View File

@ -2,7 +2,7 @@
// See LICENSE for copying information.
<template>
<div class="new-project-step">
<div class="new-project-step" @keyup.enter="createProjectClick">
<h1 class="new-project-step__title">Name Your Project</h1>
<p class="new-project-step__sub-title">
Projects are where buckets are created for storing data. Within a Project, usage is tracked at the bucket
@ -57,7 +57,7 @@ import { Component, Vue } from 'vue-property-decorator';
import HeaderedInput from '@/components/common/HeaderedInput.vue';
import VButton from '@/components/common/VButton.vue';
import { RouteConfig } from '@/router';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { CreateProjectModel, Project } from '@/types/projects';
@ -133,6 +133,12 @@ export default class CreateProjectStep extends Vue {
await this.$notify.error(`Unable to get project members. ${error.message}`);
}
try {
await this.$store.dispatch(BUCKET_ACTIONS.CLEAR);
} catch (error) {
await this.$notify.error(error.message);
}
try {
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_BILLING_HISTORY);
} catch (error) {
@ -159,18 +165,23 @@ export default class CreateProjectStep extends Vue {
this.isLoading = false;
// TODO: rework after adding third step of onboarding tour
await this.$router.push(RouteConfig.ApiKeys.path);
this.$emit('setApiKeyState');
}
/**
* Validates input value to satisfy project name rules.
*/
private isProjectNameValid(): boolean {
this.projectName.trim();
this.projectName = this.projectName.trim();
if (!this.projectName) {
this.nameError = 'Project name can\'t be empty!';
return false;
}
if (!anyCharactersButSlash(this.projectName)) {
this.nameError = 'Name for project is invalid!';
this.nameError = 'Project name can\'t have forward slash';
return false;
}

View File

@ -0,0 +1,242 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="upload-data-area">
<div class="upload-data-area__container">
<h1 class="upload-data-area__container__title">Upload Data</h1>
<p class="upload-data-area__container__sub-title">
From here, youll set up Tardigrade to store data for your project using our S3 Gateway, Uplink CLI, or
select from our growing library of connectors to build apps on Tardigrade.
</p>
<div class="upload-data-area__container__docs-area">
<div class="upload-data-area__container__docs-area__option">
<h2 class="upload-data-area__container__docs-area__option__title">
Migrate Data from your Existing AWS buckets
</h2>
<img src="@/../static/images/onboardingTour/s3.png" alt="s3 gateway image">
<h3 class="upload-data-area__container__docs-area__option__sub-title">
S3 Gateway
</h3>
<p class="upload-data-area__container__docs-area__option__info">
Make the switch with Tardigrades S3 Gateway.
</p>
<a
class="upload-data-area__container__docs-area__option__link"
href="https://documentation.tardigrade.io/api-reference/s3-gateway"
target="_blank"
>
S3 Gateway Docs
</a>
</div>
<div class="upload-data-area__container__docs-area__option uplink-option">
<h2 class="upload-data-area__container__docs-area__option__title">
Upload Data from Your Local Environment
</h2>
<img src="@/../static/images/onboardingTour/uplinkcli.png" alt="uplink cli image">
<h3 class="upload-data-area__container__docs-area__option__sub-title">
Uplink CLI
</h3>
<p class="upload-data-area__container__docs-area__option__info">
Start uploading data from the command line.
</p>
<a
class="upload-data-area__container__docs-area__option__link"
href="https://documentation.tardigrade.io/api-reference/uplink-cli"
target="_blank"
>
Uplink CLI Docs
</a>
</div>
<div class="upload-data-area__container__docs-area__option">
<h2 class="upload-data-area__container__docs-area__option__title">
Use Tardigrade for your apps storage layer
</h2>
<img src="@/../static/images/onboardingTour/connectors.png" alt="connectors image">
<h3 class="upload-data-area__container__docs-area__option__sub-title">
App Connectors
</h3>
<p class="upload-data-area__container__docs-area__option__info">
Integrate Tardigrade into your existing stack.
</p>
<a
class="upload-data-area__container__docs-area__option__link"
href="https://documentation.tardigrade.io/concepts/connectors"
target="_blank"
>
App Connectors
</a>
</div>
</div>
</div>
<VButton
class="go-to-button"
width="276px"
height="40px"
label="Go to Dashboard"
:on-press="onButtonClick"
/>
<span class="upload-data-area__support-info">
Need help?
<a class="upload-data-area__support-info__link" href="https://support.tardigrade.io/hc/en-us" target="_blank">
Contact support
</a>
</span>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import VButton from '@/components/common/VButton.vue';
import { RouteConfig } from '@/router';
@Component({
components: {
VButton,
},
})
export default class UploadDataStep extends Vue {
/**
* Holds button click logic.
* Ends onboarding tour and redirects to project dashboard.
*/
public onButtonClick(): void {
this.$router.push(RouteConfig.ProjectDashboard.path);
}
}
</script>
<style scoped lang="scss">
h1,
h2,
h3,
p {
margin: 0;
}
.upload-data-area {
display: flex;
flex-direction: column;
align-items: center;
width: auto;
font-family: 'font_regular', sans-serif;
margin-top: 25px;
&__container {
padding: 60px;
display: flex;
flex-direction: column;
align-items: center;
background-color: rgba(255, 255, 255, 0.4);
border-radius: 8px;
margin-bottom: 50px;
&__title {
font-size: 32px;
line-height: 39px;
color: #1b2533;
margin-bottom: 25px;
}
&__sub-title {
font-size: 16px;
line-height: 19px;
color: #354049;
margin-bottom: 35px;
text-align: center;
word-break: break-word;
max-width: 800px;
}
&__docs-area {
display: flex;
align-items: center;
justify-content: space-between;
&__option {
padding: 30px;
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid rgba(144, 155, 168, 0.4);
border-radius: 8px;
&__title {
font-size: 10px;
line-height: 15px;
text-align: center;
letter-spacing: 0.05em;
text-transform: uppercase;
color: #909ba8;
margin-bottom: 25px;
max-width: 181px;
word-break: break-word;
}
&__sub-title {
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 26px;
text-align: center;
color: #354049;
margin: 25px 0 5px 0;
}
&__info {
font-size: 14px;
line-height: 17px;
text-align: center;
color: #61666b;
max-width: 181px;
word-break: break-word;
margin-bottom: 15px;
}
&__link {
width: 181px;
height: 40px;
border: 1px solid #2683ff;
border-radius: 6px;
background-color: #fff;
color: #2683ff;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: #2683ff;
color: #fff;
}
}
}
}
}
&__support-info {
margin-top: 50px;
font-family: 'font_bold', sans-serif;
font-size: 15px;
line-height: 22px;
text-align: center;
color: #909ba8;
&__link {
text-decoration: underline;
}
}
}
.uplink-option {
margin: 0 45px;
}
@media screen and (max-width: 1350px) {
.uplink-option {
margin: 0 15px;
}
}
</style>

View File

@ -205,7 +205,6 @@ export default class HeaderArea extends Vue {
private async setProjectState(): Promise<void> {
const projects = await this.$store.dispatch(PROJECTS_ACTIONS.FETCH);
if (!projects.length) {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED_EMPTY);
await this.$router.push(RouteConfig.OnboardingTour.path);
return;

View File

@ -96,10 +96,10 @@ export function makeApiKeysModule(api: ApiKeysApi): StoreModule<ApiKeysState> {
const projectId = rootGetters.selectedProject.id;
commit(SET_PAGE_NUMBER, pageNumber);
const apiKeys = await api.get(projectId, state.cursor);
commit(SET_PAGE, apiKeys);
const apiKeysPage: ApiKeysPage = await api.get(projectId, state.cursor);
commit(SET_PAGE, apiKeysPage);
return apiKeys;
return apiKeysPage;
},
createApiKey: async function ({commit, rootGetters}: any, name: string): Promise<ApiKey> {
const projectId = rootGetters.selectedProject.id;

View File

@ -4,6 +4,5 @@
export enum AppState {
LOADING = 1,
LOADED,
LOADED_EMPTY,
ERROR,
}

View File

@ -19,3 +19,8 @@ export enum AddingStorjState {
VERIFYING,
VERIFIED,
}
export enum AddingApiKeyState {
CREATE = 1,
COPY,
}

View File

@ -45,7 +45,9 @@ import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { USER_ACTIONS } from '@/store/modules/users';
import { ApiKeysPage } from '@/types/apiKeys';
import { Project } from '@/types/projects';
import { User } from '@/types/users';
import { Size } from '@/utils/bytesSize';
import {
API_KEYS_ACTIONS,
@ -82,9 +84,11 @@ export default class DashboardArea extends Vue {
* Pre fetches user`s and project information.
*/
public async mounted(): Promise<void> {
let user: User;
// TODO: combine all project related requests in one
try {
await this.$store.dispatch(USER_ACTIONS.GET);
user = await this.$store.dispatch(USER_ACTIONS.GET);
} catch (error) {
if (!(error instanceof ErrorUnauthorized)) {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.ERROR);
@ -143,7 +147,7 @@ export default class DashboardArea extends Vue {
}
if (!projects.length) {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED_EMPTY);
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED);
try {
await this.$router.push(RouteConfig.OnboardingTour.path);
@ -156,7 +160,26 @@ export default class DashboardArea extends Vue {
await this.$store.dispatch(PROJECTS_ACTIONS.SELECT, projects[0].id);
await this.$store.dispatch(PM_ACTIONS.SET_SEARCH_QUERY, '');
let apiKeysPage: ApiKeysPage = new ApiKeysPage();
try {
apiKeysPage = await this.$store.dispatch(API_KEYS_ACTIONS.FETCH, 1);
} catch (error) {
await this.$notify.error(`Unable to fetch api keys. ${error.message}`);
}
if (projects.length === 1 && projects[0].ownerId === user.id && apiKeysPage.apiKeys.length === 0) {
await this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADED);
try {
await this.$router.push(RouteConfig.OnboardingTour.path);
} catch (error) {
return;
}
return;
}
try {
await this.$store.dispatch(PM_ACTIONS.FETCH, 1);
} catch (error) {
@ -169,12 +192,6 @@ export default class DashboardArea extends Vue {
await this.$notify.error(`Unable to fetch project limits. ${error.message}`);
}
try {
await this.$store.dispatch(API_KEYS_ACTIONS.FETCH, 1);
} catch (error) {
await this.$notify.error(`Unable to fetch api keys. ${error.message}`);
}
try {
await this.$store.dispatch(BUCKET_ACTIONS.FETCH, 1);
} catch (error) {

View File

@ -60,7 +60,7 @@ export default class RegisterArea extends Vue {
* Lifecycle hook after initial render.
* Sets up variables from route params.
*/
async mounted(): Promise<void> {
public async mounted(): Promise<void> {
if (this.$route.query.token) {
this.secret = this.$route.query.token.toString();
}
@ -85,6 +85,16 @@ export default class RegisterArea extends Vue {
}
}
/**
* Lifecycle hook on component destroy.
* Sets view to default state.
*/
public beforeDestroy(): void {
if (this.isRegistrationSuccessful) {
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SUCCESSFUL_REGISTRATION);
}
}
/**
* Indicates if registration successful area shown.
*/

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,3 @@
<svg class="info-svg" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 -7.86805e-07C4.02944 -1.22135e-06 1.22135e-06 4.02944 7.86805e-07 9C3.52265e-07 13.9706 4.02944 18 9 18C13.9706 18 18 13.9706 18 9C18 4.02944 13.9706 -3.52265e-07 9 -7.86805e-07ZM10 13C10 13.5523 9.55229 14 9 14C8.44772 14 8 13.5523 8 13C8 12.4477 8.44772 12 9 12C9.55229 12 10 12.4477 10 13ZM10 9C10 9.55228 9.55228 10 9 10C8.44771 10 8 9.55228 8 9L8 5C8 4.44771 8.44772 4 9 4C9.55229 4 10 4.44772 10 5L10 9Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -9,7 +9,7 @@ exports[`ApiKeysArea renders correctly 1`] = `
<!---->
<!---->
<!---->
<noapikeysarea-stub onbuttonclick="function () { [native code] }" class="collapsed"></noapikeysarea-stub>
<noapikeysarea-stub onbuttonclick="function () { [native code] }"></noapikeysarea-stub>
</div>
</div>
`;
@ -23,7 +23,7 @@ exports[`ApiKeysArea renders empty screen with add key prompt 1`] = `
<!---->
<!---->
<!---->
<div class="no-api-keys-area collapsed">
<div class="no-api-keys-area">
<h1 class="no-api-keys-area__title">Create Your First API Key</h1>
<p class="no-api-keys-area__sub-title">API keys give access to the project to create buckets, upload objects</p>
<div class="no-api-keys-area__button container" style="width: 180px; height: 48px;"><span class="label">Create API Key</span></div>
@ -67,7 +67,7 @@ exports[`ApiKeysArea renders empty search state correctly 1`] = `
<!---->
<!---->
<!---->
<noapikeysarea-stub onbuttonclick="function () { [native code] }" class="collapsed"></noapikeysarea-stub>
<noapikeysarea-stub onbuttonclick="function () { [native code] }"></noapikeysarea-stub>
</div>
</div>
`;

View File

@ -18,7 +18,7 @@ export class ApiKeysMock implements ApiKeysApi {
}
create(projectId: string, name: string): Promise<ApiKey> {
throw new Error('Method not implemented');
return Promise.resolve(new ApiKey('testId', 'testName', 'test', 'testKey'));
}
delete(ids: string[]): Promise<void> {

View File

@ -11,4 +11,37 @@ describe('ProgressBar.vue', () => {
expect(wrapper).toMatchSnapshot();
});
it('renders correctly if create project step is completed', (): void => {
const wrapper = mount(ProgressBar, {
propsData: {
isCreateProjectStep: true,
},
});
expect(wrapper.findAll('.completed-step').length).toBe(1);
expect(wrapper.findAll('.completed-font-color').length).toBe(1);
});
it('renders correctly if create api key step is completed', (): void => {
const wrapper = mount(ProgressBar, {
propsData: {
isCreateApiKeyStep: true,
},
});
expect(wrapper.findAll('.completed-step').length).toBe(3);
expect(wrapper.findAll('.completed-font-color').length).toBe(2);
});
it('renders correctly if upload data step is completed', (): void => {
const wrapper = mount(ProgressBar, {
propsData: {
isUploadDataStep: true,
},
});
expect(wrapper.findAll('.completed-step').length).toBe(5);
expect(wrapper.findAll('.completed-font-color').length).toBe(3);
});
});

View File

@ -17,6 +17,10 @@ exports[`ProgressBar.vue renders correctly 1`] = `
</div>
<div class="progress-bar-container__titles-area"><span class="progress-bar-container__titles-area__title">
Name Your Project
</span> <span class="progress-bar-container__titles-area__title api-key-title">Create an API Key</span> <span class="progress-bar-container__titles-area__title">Upload Data</span></div>
</span> <span class="progress-bar-container__titles-area__title api-key-title">
Create an API Key
</span> <span class="progress-bar-container__titles-area__title">
Upload Data
</span></div>
</div>
`;

View File

@ -6,5 +6,7 @@ exports[`OnboardingTourArea.vue renders correctly 1`] = `
<overviewstep-stub></overviewstep-stub>
<!---->
<!---->
<!---->
<!---->
</div>
`;

View File

@ -0,0 +1,70 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
import Vuex from 'vuex';
import CreateApiKeyStep from '@/components/onboardingTour/steps/CreateApiKeyStep.vue';
import { makeApiKeysModule } from '@/store/modules/apiKeys';
import { makeProjectsModule } from '@/store/modules/projects';
import { ApiKeysPage } from '@/types/apiKeys';
import { Project } from '@/types/projects';
import { NotificatorPlugin } from '@/utils/plugins/notificator';
import { SegmentioPlugin } from '@/utils/plugins/segment';
import { createLocalVue, mount } from '@vue/test-utils';
import { ApiKeysMock } from '../../mock/api/apiKeys';
import { ProjectsApiMock } from '../../mock/api/projects';
const localVue = createLocalVue();
const notificationPlugin = new NotificatorPlugin();
const segmentioPlugin = new SegmentioPlugin();
const projectsApi = new ProjectsApiMock();
const projectsModule = makeProjectsModule(projectsApi);
const apiKeysApi = new ApiKeysMock();
const apiKeysModule = makeApiKeysModule(apiKeysApi);
apiKeysApi.setMockApiKeysPage(new ApiKeysPage());
const project = new Project('id', 'projectName', 'projectDescription', 'test', 'testOwnerId', true);
projectsApi.setMockProjects([project]);
localVue.use(Vuex);
localVue.use(notificationPlugin);
localVue.use(segmentioPlugin);
const store = new Vuex.Store({ modules: { projectsModule, apiKeysModule }});
describe('CreateApiKeyStep.vue', () => {
it('renders correctly', (): void => {
const wrapper = mount(CreateApiKeyStep, {
store,
localVue,
});
expect(wrapper.findAll('.disabled').length).toBe(1);
expect(wrapper).toMatchSnapshot();
});
it('create api key works correctly correctly', async (): Promise<void> => {
const wrapper = mount(CreateApiKeyStep, {
store,
localVue,
});
await wrapper.vm.setApiKeyName('testName');
await wrapper.vm.createApiKey();
expect(wrapper.findAll('.disabled').length).toBe(0);
expect(wrapper).toMatchSnapshot();
});
it('done click works correctly correctly', async (): Promise<void> => {
const wrapper = mount(CreateApiKeyStep, {
store,
localVue,
});
await wrapper.find('.done-button').trigger('click');
expect(wrapper.emitted()).toHaveProperty('setUploadDataState');
});
});

View File

@ -0,0 +1,36 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
import sinon from 'sinon';
import UploadDataStep from '@/components/onboardingTour/steps/UploadDataStep.vue';
import { router } from '@/router';
import { createLocalVue, mount } from '@vue/test-utils';
const localVue = createLocalVue();
describe('UploadDataStep.vue', () => {
it('renders correctly', (): void => {
const wrapper = mount(UploadDataStep, {
localVue,
});
expect(wrapper).toMatchSnapshot();
});
it('button click works correctly', async (): Promise<void> => {
const clickSpy = sinon.spy();
const wrapper = mount(UploadDataStep, {
localVue,
router,
methods: {
onButtonClick: clickSpy,
},
});
await wrapper.find('.go-to-button').trigger('click');
expect(clickSpy.callCount).toBe(1);
});
});

View File

@ -0,0 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreateApiKeyStep.vue create api key works correctly correctly 1`] = `
<div class="create-api-key-step">
<h1 class="create-api-key-step__title">Create an API Key</h1>
<p class="create-api-key-step__sub-title">
API keys provide access to the project for creating buckets and uploading objects through the command line
interface. This will be your first API key, and you can always create more keys later on.
</p>
<div class="create-api-key-step__container">
<div class="create-api-key-step__container__title-area">
<h2 class="create-api-key-step__container__title-area__title">Create API Key</h2>
<!---->
</div>
<div class="input-container full-input">
<div class="label-container">
<!---->
<h3 class="label-container__label">API Key Name</h3>
<h3 class="label-container__label add-label"></h3>
<!---->
</div>
<!---->
<!----> <input id="API Key Name" placeholder="Enter API Key Name (i.e. Dans Key)" type="text" class="headered-input" style="height: 48px;">
</div>
<div class="create-api-key-step__container__copy-key-area">
<div class="create-api-key-step__container__copy-key-area__header"><svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" class="info-svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 -7.86805e-07C4.02944 -1.22135e-06 1.22135e-06 4.02944 7.86805e-07 9C3.52265e-07 13.9706 4.02944 18 9 18C13.9706 18 18 13.9706 18 9C18 4.02944 13.9706 -3.52265e-07 9 -7.86805e-07ZM10 13C10 13.5523 9.55229 14 9 14C8.44772 14 8 13.5523 8 13C8 12.4477 8.44772 12 9 12C9.55229 12 10 12.4477 10 13ZM10 9C10 9.55228 9.55228 10 9 10C8.44771 10 8 9.55228 8 9L8 5C8 4.44771 8.44772 4 9 4C9.55229 4 10 4.44772 10 5L10 9Z" fill="white"></path>
</svg> <span class="create-api-key-step__container__copy-key-area__header__title">
API Keys only appear here once. Copy and paste this key to your preferred method of storing secrets.
</span></div>
<div class="create-api-key-step__container__copy-key-area__key-container"><span class="create-api-key-step__container__copy-key-area__key-container__key">testKey</span>
<div class="create-api-key-step__container__copy-key-area__key-container__copy-button">
<div class="container blue-white" style="width: 81px; height: 40px;"><span class="label">Copy</span></div>
</div>
</div>
</div>
<p class="create-api-key-step__container__info">
We dont record your API Keys, which are only displayed once when generated. If you loose this
key, it cannot be recovered but you can always create new API Keys when needed.
</p>
<!---->
</div>
<div class="done-button container" style="width: 156px; height: 48px;"><span class="label">Done</span></div>
</div>
`;
exports[`CreateApiKeyStep.vue renders correctly 1`] = `
<div class="create-api-key-step">
<h1 class="create-api-key-step__title">Create an API Key</h1>
<p class="create-api-key-step__sub-title">
API keys provide access to the project for creating buckets and uploading objects through the command line
interface. This will be your first API key, and you can always create more keys later on.
</p>
<div class="create-api-key-step__container">
<div class="create-api-key-step__container__title-area">
<h2 class="create-api-key-step__container__title-area__title">Create API Key</h2>
<!---->
</div>
<div class="input-container full-input">
<div class="label-container">
<!---->
<h3 class="label-container__label">API Key Name</h3>
<h3 class="label-container__label add-label"></h3>
<!---->
</div>
<!---->
<!----> <input id="API Key Name" placeholder="Enter API Key Name (i.e. Dans Key)" type="text" class="headered-input" style="height: 48px;">
</div>
<div class="create-api-key-step__container__create-key-area">
<div class="generate-button container blue-white" style="width: 100%; height: 40px;"><span class="label">Generate API Key</span></div>
</div>
<!---->
<!---->
</div>
<div class="done-button container disabled" style="width: 156px; height: 48px;"><span class="label">Done</span></div>
</div>
`;

View File

@ -0,0 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UploadDataStep.vue renders correctly 1`] = `
<div class="upload-data-area">
<div class="upload-data-area__container">
<h1 class="upload-data-area__container__title">Upload Data</h1>
<p class="upload-data-area__container__sub-title">
From here, youll set up Tardigrade to store data for your project using our S3 Gateway, Uplink CLI, or
select from our growing library of connectors to build apps on Tardigrade.
</p>
<div class="upload-data-area__container__docs-area">
<div class="upload-data-area__container__docs-area__option">
<h2 class="upload-data-area__container__docs-area__option__title">
Migrate Data from your Existing AWS buckets
</h2> <img src="@/../static/images/onboardingTour/s3.png" alt="s3 gateway image">
<h3 class="upload-data-area__container__docs-area__option__sub-title">
S3 Gateway
</h3>
<p class="upload-data-area__container__docs-area__option__info">
Make the switch with Tardigrades S3 Gateway.
</p> <a href="https://documentation.tardigrade.io/api-reference/s3-gateway" target="_blank" class="upload-data-area__container__docs-area__option__link">
S3 Gateway Docs
</a>
</div>
<div class="upload-data-area__container__docs-area__option uplink-option">
<h2 class="upload-data-area__container__docs-area__option__title">
Upload Data from Your Local Environment
</h2> <img src="@/../static/images/onboardingTour/uplinkcli.png" alt="uplink cli image">
<h3 class="upload-data-area__container__docs-area__option__sub-title">
Uplink CLI
</h3>
<p class="upload-data-area__container__docs-area__option__info">
Start uploading data from the command line.
</p> <a href="https://documentation.tardigrade.io/api-reference/uplink-cli" target="_blank" class="upload-data-area__container__docs-area__option__link">
Uplink CLI Docs
</a>
</div>
<div class="upload-data-area__container__docs-area__option">
<h2 class="upload-data-area__container__docs-area__option__title">
Use Tardigrade for your apps storage layer
</h2> <img src="@/../static/images/onboardingTour/connectors.png" alt="connectors image">
<h3 class="upload-data-area__container__docs-area__option__sub-title">
App Connectors
</h3>
<p class="upload-data-area__container__docs-area__option__info">
Integrate Tardigrade into your existing stack.
</p> <a href="https://documentation.tardigrade.io/concepts/connectors" target="_blank" class="upload-data-area__container__docs-area__option__link">
App Connectors
</a>
</div>
</div>
</div>
<div class="go-to-button container" style="width: 276px; height: 40px;"><span class="label">Go to Dashboard</span></div> <span class="upload-data-area__support-info">
Need help?
<a href="https://support.tardigrade.io/hc/en-us" target="_blank" class="upload-data-area__support-info__link">
Contact support
</a></span>
</div>
`;