Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new team selection frontend #4943

Merged
merged 6 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/components/PageHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<router-link :to="homeLink">
<img class="ff-logo" src="/ff-logo--wordmark--dark.png">
</router-link>
<global-search v-if="hasAMinimumTeamRoleOf(Roles.Viewer)" />
<global-search v-if="teams.length > 0 && hasAMinimumTeamRoleOf(Roles.Viewer)" />
<!-- Mobile: Toggle(User Options) -->
<div class="flex ff-mobile-navigation-right" data-el="mobile-nav-right">
<NotificationsButton class="ff-header--mobile-notificationstoggle" :class="{'active': mobileTeamSelectionOpen}" />
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/components/TeamTypeSelection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<div>
<div class="flex gap-6 justify-center relative z-10 flex-wrap">
<team-type-tile v-for="type in types" :key="type.id" :team-type="type" />
</div>
</div>
</template>

<script>
import { mapState } from 'vuex'

import teamTypesApi from '../api/teamTypes.js'

import TeamTypeTile from './TeamTypeTile.vue'

export default {
name: 'TeamTypeSelection',
components: {
'team-type-tile': TeamTypeTile
},
data () {
return {
types: []
}
},
computed: {
...mapState('account', ['user'])
},
async created () {
const { types } = await teamTypesApi.getTeamTypes()
this.types = types.sort((a, b) => a.order - b.order)
}
}
</script>
143 changes: 143 additions & 0 deletions frontend/src/components/TeamTypeTile.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<template>
<div class="ff-team-type-tile">
<div v-if="isTrial(teamType)" class="trial-ribbon">
<label>{{ teamType.properties.trial.duration }} Days Free Trial</label>
</div>
<div class="space-y-2">
<img class="w-36 m-auto" src="../images/empty-states/application-instances.png">
<div class="flex flex-col gap-5">
<div class="flex justify-between items-center text-2xl">
<label class="font-medium">{{ teamType.name }}</label>
<span v-if="pricing?.value">
{{ pricing.value }} <span class="text-xs">/{{ pricing.interval }}</span>
</span>
</div>
<ff-markdown-viewer :content="teamType.description" />
</div>
</div>
<template v-if="enableCTA">
<ff-button v-if="isTrial(teamType)" kind="primary" class="w-full mt-4" :to="`/team/create?teamType=${teamType.id}`">
Start Free Trial
</ff-button>
<ff-button v-else-if="isManualBilling(teamType)" kind="secondary" class="w-full mt-4" @click="contactFF(teamType)">
Contact FlowFuse
</ff-button>
<ff-button v-else kind="secondary" class="w-full mt-4" :to="`/team/create?teamType=${teamType.id}`">
Select
</ff-button>
</template>
</div>
</template>

<script>
import { mapState } from 'vuex'

import BillingAPI from '../api/billing.js'
import Alerts from '../services/alerts.js'

export default {
name: 'TeamTypeTile',
props: {
teamType: {
type: Object,
required: true
},
enableCTA: {
type: Boolean,
default: true
}
},
computed: {
...mapState('account', ['user', 'teams']),
pricing: function () {
const billing = this.teamType.properties?.billing.description?.split('/')
const price = {}
if (typeof billing !== 'undefined') {
price.value = billing[0]
price.interval = billing[1]
}
return price
}
},
methods: {
isTrial (teamType) {
// A team trial can be offered if:
// 1. User has no other teams
return this.teams.length === 0 &&
// 2. User is less than a week old
(Date.now() - (new Date(this.user.createdAt)).getTime()) < 1000 * 60 * 60 * 24 * 7 &&
// 3. TeamType meta data says so
teamType.properties?.trial?.active
},
isManualBilling (teamType) {
return teamType.properties?.billing?.requireContact
},
contactFF (teamType) {
BillingAPI.sendTeamTypeContact(this.user, teamType, 'Create Team').then(() => {
Alerts.emit('A message has been sent to our team. We will contact you soon regarding your request. In the mean time, feel free to choose another plan to get started.', 'confirmation', 20000)
}).catch(err => {
Alerts.emit('Something went wrong with the request. Please try again or contact support for help.', 'warning', 15000)
console.error('Failed to submit hubspot form: ', err)
})
}
}
}
</script>

<style lang="scss">
.ff-team-type-tile {
position: relative;
border-radius: 6px;
border: 2px solid $ff-grey-300;
background: white;
box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.25);
padding: 24px;
width: 100%;
max-width: 300px;
display: flex;
flex-direction: column;
justify-content: space-between;
ul {
list-style: disc;
padding-left: 16px;
li {
margin-bottom: 6px;
}
}
}
.trial-ribbon {
--ribbon-overlap: 8px;
display: flex;
justify-content: center;
align-items: center;
height: 30px;
left: calc(-1 * var(--ribbon-overlap));
line-height: 1.3;
width: calc(100% + 2 * var(--ribbon-overlap));
margin: 0;
position: absolute;
top: 8px;
color: white;
// text-shadow: 0 1px 1px #111;
border-top: 1px solid #363636;
border-bottom: 1px solid #202020;
background: $ff-red-500;
// background: linear-gradient($ff-red-500 0%, $ff-red-700 100%);
border-radius: 2px 2px 0 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.trial-ribbon::before,
.trial-ribbon::after {
content: '';
display: block;
width: 0;
height: 0;
position: absolute;
bottom: calc((-2 * var(--ribbon-overlap)) - 1px);
z-index: -10;
border: var(--ribbon-overlap) solid;
border-color: $ff-red-900 transparent transparent transparent;
}
.trial-ribbon::before {left: 0;}
.trial-ribbon::after {right: 0;}
</style>
19 changes: 13 additions & 6 deletions frontend/src/pages/Home.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
<template>
<main>
<main class="min-h-full">
<template v-if="pending">
<div class="flex-grow flex flex-col items-center justify-center mx-auto text-gray-600 opacity-50">
<FlowFuseLogo class="max-w-xs mx-auto w-full" />
</div>
</template>
<template v-else-if="teams.length === 0">
<NoTeamsUser />
</template>
<ff-page v-else-if="teams.length === 0">
<template #header>
<ff-page-header title="Choose Team Type">
<template #context>
Choose which team type you'd like to get started with.
</template>
</ff-page-header>
</template>
<TeamTypeSelection />
</ff-page>
</main>
</template>

Expand All @@ -17,13 +24,13 @@ import { mapGetters, mapState } from 'vuex'

import FlowFuseLogo from '../components/Logo.vue'

import NoTeamsUser from './account/NoTeamsUser.vue'
import TeamTypeSelection from '../components/TeamTypeSelection.vue'

export default {
name: 'HomePage',
components: {
FlowFuseLogo,
NoTeamsUser
TeamTypeSelection
},
data () {
return {
Expand Down
44 changes: 0 additions & 44 deletions frontend/src/pages/account/NoTeamsUser.vue

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
Order
<template #description>Set the sort order when listing the types</template>
</FormRow>
<FormRow v-model="input.description" :error="errors.description" data-form="description">
<FormRow v-model="input.description" :error="errors.description" data-form="description" containerClass="w-full">
Description
<template #description>Use markdown for formatting</template>
<template #input><textarea v-model="input.description" class="w-full" rows="6" /></template>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/admin/TeamTypes/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default {
},
computed: {
activeTeamTypes () {
const types = this.teamTypes.filter(pt => pt.active)
const types = this.teamTypes.filter(pt => pt.active).sort((a, b) => a.order - b.order)
return types
},
inactiveTeamTypes () {
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/pages/instance/components/InstanceForm.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<template>
<FeatureUnavailableToTeam v-if="teamRuntimeLimitReached" fullMessage="You have reached the runtime limit for this team." />
<FeatureUnavailableToTeam v-else-if="teamInstanceLimitReached" fullMessage="You have reached the instance limit for this team." />
<form class="space-y-6" @submit.prevent="onSubmit">
<SectionTopMenu v-if="hasHeader" :hero="heroTitle" />
<!-- Form title -->
Expand Down Expand Up @@ -67,6 +65,8 @@
</FormRow>

<div v-if="!creatingApplication || input.createInstance" :class="creatingApplication ? 'ml-6' : ''" class="space-y-6">
<FeatureUnavailableToTeam v-if="teamRuntimeLimitReached" fullMessage="You have reached the runtime limit for this team." />
<FeatureUnavailableToTeam v-else-if="teamInstanceLimitReached" fullMessage="You have reached the instance limit for this team." />
<!-- Instance Name -->
<div>
<FormRow
Expand Down Expand Up @@ -452,6 +452,10 @@ export default {
return (teamTypeRuntimeLimit > 0 && currentRuntimeCount >= teamTypeRuntimeLimit)
},
teamInstanceLimitReached () {
// this.projectTypes.length > 0 : There are Instance Types defined
// this.activeProjectTypeCount : How instance types are available for the user to select
// taking into account their limits
// Hence, if activeProjectTypeCount === 0, then they are at their limit of usage
return this.projectTypes.length > 0 && this.activeProjectTypeCount === 0
},
atLeastOneFlowBlueprint () {
Expand Down Expand Up @@ -648,6 +652,9 @@ export default {
...this.preDefinedInputs
}
}
if (this.teamInstanceLimitReached || this.teamRuntimeLimitReached) {
this.input.createInstance = false
}
},
async beforeMount () {
// Billing feature must be enabled
Expand Down
Loading
Loading