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

VPN #288

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft

VPN #288

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
11 changes: 5 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"main": "server/index.js",
"dependencies": {
"@chenfengyuan/vue-qrcode": "^2.0.0",
"@edge/account-utils": "^0.4.1",
"@edge/account-utils": "github:edge/account-utils#feature/vpn",
"@edge/cache-config": "^1.1.0",
"@edge/index-utils": "^0.6.2",
"@headlessui/vue": "^1.7.3",
Expand Down
5 changes: 5 additions & 0 deletions src/components/SideNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export default {
link: '/servers',
text: 'Servers'
},
{
_key: 'vpn',
link: '/vpns',
text: 'VPN'
},
{
_key: 'dns',
link: '/domains',
Expand Down
5 changes: 4 additions & 1 deletion src/components/server/ServerPowerToggle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { mapGetters, mapState } from 'vuex'

export default {
name: 'ServerPowerToggle',
props: ['activeTasks', 'disableActions', 'server'],
props: ['disableActions', 'server'],
data() {
return {
showConfirmationModal: false,
Expand All @@ -71,6 +71,9 @@ export default {
computed: {
...mapGetters(['balanceSuspend']),
...mapState(['session']),
activeTasks() {
return this.$store.getters.tasksByServerId(this.serverId)
},
destroying() {
return this.activeTasks.some(task => task.action === 'destroy')
},
Expand Down
98 changes: 98 additions & 0 deletions src/layout/EditableTitle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script setup>
import LoadingSpinner from '@/components/icons/LoadingSpinner.vue'
import { CheckIcon, PencilIcon, XIcon } from '@heroicons/vue/outline'
import { defineEmits, defineModel, defineProps, ref, watchPostEffect } from 'vue'

const emit = defineEmits(['cancel', 'edit','save'])

const props = defineProps({
busy: Boolean,
disabled: Boolean,
invalid: Boolean,
placeholder: String
})

const editing = ref(false)
const input = ref(null)
const model = defineModel()
const previousValue = ref(model.value)

function cancel() {
editing.value = false
model.value = previousValue.value

emit('cancel')
}

function edit() {
editing.value = true

emit('edit')
}

function save() {
if (props.invalid) return

editing.value = false
previousValue.value = model.value

emit('save')
}

watchPostEffect(() => {
if (input.value) {
input.value.focus()
}
})
</script>

<template>
<div v-if="editing" class="editable-title__container flex">
<input
v-model="model"
@keypress.enter="save"
class="editable-title__value"
:placeholder="placeholder"
ref="input"
type="text"
/>

<div class="mt-3 flex">
<button class="ml-2" @click="save" :disabled="invalid || disabled">
<CheckIcon class="button__icon text-green hover:text-green-600" />
</button>
<button @click="cancel" class="ml-2">
<XIcon class="button__icon text-red hover:text-red-600" />
</button>
</div>
</div>

<div v-else class="editable-title__container flex">
<h1 class="w-max mb-0">{{ model }}</h1>

<div class="mt-3">
<button v-if="busy" class="ml-2" disabled>
<LoadingSpinner />
</button>
<button v-else class="ml-2" @click="edit" :disabled="disabled">
<PencilIcon class="button__icon text-gray-400 hover:text-green" />
</button>
</div>
</div>
</template>

<style>
.editable-title__value {
@apply bg-transparent text-3xl text-gray-600 border-b border-gray-400 w-full;
@apply focus:outline-none focus:border-green focus:text-green;
}

.editable-title__container .button__icon {
@apply w-5 ml-1;
}

.editable-title__container button:disabled,
.editable-title__container button:disabled .button__icon {
@apply text-gray-400 hover:no-underline;
}
</style>
60 changes: 60 additions & 0 deletions src/layout/Slider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script setup>
import 'vue-slider-component/theme/antd.css'
import VueSlider from 'vue-slider-component'
import { defineEmits, defineModel, defineProps } from 'vue'

defineProps({
disabled: Boolean,
formatter: Function,
marks: Object,
max: Number,
min: Number,
title: String,
tooltip: String
})

const emit = defineEmits(['change'])

const model = defineModel()
</script>

<template>
<div class="slider__container" :class="{ disabled }">
<span v-if="title" class="slider__title">{{ title }}</span>
<vue-slider
v-model="model"
adsorb
tooltipPlacement="top"
width="100%"
:contained="true"
:disabled="disabled"
:dot-style="{ background: '#4ecd5f', boxShadow: '0 0 2px 1px #eee', border: 'none' }"
:dotSize="20"
:label-style="{ color: '#999', fontSize: '12px' }"
:marks="marks"
:max="max"
:min="min"
:process-style="{ background: '#4ecd5f' }"
:step-active-style="{ background: '#fff', opacity: '1', border: 'none', boxShadow: 'rgba(0, 0, 0, 0.24) 0px 3px 8px' }"
:tooltip="tooltip || 'hover'"
:tooltip-formatter="formatter"
:tooltip-style="{ background: '#4ecd5f', borderColor: '#4ecd5f' }"
@change="val => emit('change', val)"
/>
</div>
</template>

<style>
.slider__container {
@apply relative flex space-x-3 items-start justify-center pr-5 pl-2 pt-14 pb-8 border border-gray-300 rounded-md;
}

.slider__container.disabled {
@apply cursor-not-allowed opacity-50;
}

.slider__title {
@apply absolute top-0 inline-block px-3 text-gray-500 transform -translate-y-1/2 bg-white;
}

</style>
105 changes: 105 additions & 0 deletions src/modules/vpns/components/MultiuserToggle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script setup>
import CheckIcon from '../../../components/icons/CheckIcon.vue'
import { RadioGroup, RadioGroupDescription, RadioGroupLabel, RadioGroupOption } from '@headlessui/vue'
import { defineModel, defineProps } from 'vue'

defineProps({
disabled: Boolean,
singleUserDisabled: Boolean,
multiuserDisabled: Boolean
})

const model = defineModel()
</script>

<template>
<!-- Single/multi-user -->
<RadioGroup v-model="model">
<div class="box__grid mb-8">
<RadioGroupOption as="template" :value="false" :disabled="disabled || singleUserDisabled" v-slot="{ active, checked, disabled }">
<div :class="{ radioOption: true, active, checked, disabled }">
<div :class="{ checkmark: true, active, checked }">
<CheckIcon class="checkmark__icon" />
</div>
<div class="optionDetails">
<RadioGroupLabel as="h4" class="optionDetails__name">
Single-user VPN
</RadioGroupLabel>
<RadioGroupDescription as="span" class="optionDetails__description">
<span>Ideal for individual use.</span>
</RadioGroupDescription>
</div>
</div>
</RadioGroupOption>

<RadioGroupOption as="template" :value="true" :disabled="disabled || multiuserDisabled" v-slot="{ active, checked, disabled }">
<div :class="{ radioOption: true, active, checked, disabled }">
<div :class="{ checkmark: true, active, checked }">
<CheckIcon class="checkmark__icon" />
</div>
<div class="optionDetails">
<RadioGroupLabel as="h4" class="optionDetails__name">
Multi-user VPN
</RadioGroupLabel>
<RadioGroupDescription as="span" class="optionDetails__description">
<span>Multi-user VPN includes up to five users. You can pay for additional users.</span>
</RadioGroupDescription>
</div>
</div>
</RadioGroupOption>
</div>
</RadioGroup>
</template>


<style scoped>
.box__grid {
@apply mt-6 w-full grid grid-cols-1 gap-4;
@apply sm:grid-cols-2 lg:grid-cols-2;
}

/* radio option */
.radioOption {
@apply relative flex space-x-3 items-start p-5 border border-gray-300 rounded-md cursor-pointer;
@apply hover:bg-gray-100 hover:bg-opacity-50;
@apply focus:outline-none;
}
.radioOption.active {
@apply border-green border-opacity-40;
}
.radioOption.checked {
@apply bg-gray-100 border-green bg-opacity-75;
@apply ring-4 ring-green ring-opacity-10;
}
.radioOption.disabled {
@apply cursor-not-allowed opacity-50;
}

/* radio option details */
.optionDetails {
@apply flex flex-col flex-1 text-sm;
}
.optionDetails__city {
@apply text-sm text-gray-500;
}

/* checkmark */
.checkmark {
@apply flex items-center justify-center text-white flex-shrink-0 rounded-full w-5 h-5 bg-gray-100 border border-gray-300;
}
.checkmark.active {
@apply border-green border-opacity-40 bg-opacity-20;
}
.checkmark.checked {
@apply border-green bg-green;
}
.checkmark .checkmark__icon {
@apply opacity-0 w-3 h-3;
}
.checkmark.active .checkmark__icon {
@apply opacity-40;
}
.checkmark.checked .checkmark__icon {
@apply opacity-100;
}
</style>
50 changes: 50 additions & 0 deletions src/modules/vpns/components/SpeedSlider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script setup>
import Slider from '../../../layout/Slider.vue'
import { defineModel, defineProps, effect, ref } from 'vue'

defineProps({
disabled: Boolean
})

const model = defineModel()
const ready = ref(false)

const rawMarks = [
['fast', 'Fast'],
['faster', 'Faster'],
['fastest', 'Fastest']
]

const marks = rawMarks.reduce((ms, m, i) => {
ms[i] = m[1]
return ms
}, {})

function formatter(i) {
return rawMarks[i][1]
}

function setSpeed(i) {
model.value = rawMarks[i][0]
}

effect(() => {
if (!ready.value) {
/** WIP */
ready.value = true
}
})
</script>

<template>
<Slider
:disabled="disabled"
:formatter="formatter"
:marks="marks"
:max="2"
:min="0"
title="Speed"
tooltip="always"
@change="setSpeed"
/>
</template>
Loading