Skip to content

Commit 1158aff

Browse files
ILDavizDavid Galetsaeedvaziry
authored
Add initial support for Vapor connections (#151)
* feat: add initial support for Vapor connections * fix: restore line accidentally removed * fix: correct locale string * fix: remove import not needed * style --------- Co-authored-by: David Galet <david.galet@praticheatuo.online> Co-authored-by: Saeed Vaziry <mr.saeedvaziry@gmail.com>
1 parent 24e4982 commit 1158aff

11 files changed

Lines changed: 401 additions & 5 deletions

File tree

package-lock.json

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
"@heroicons/vue": "^2.1.3",
120120
"@types/adm-zip": "^0.5.7",
121121
"@types/express": "^5.0.0",
122+
"@types/js-yaml": "^4.0.9",
122123
"@types/node": "^22.10.2",
123124
"@types/node-fetch": "^2.6.12",
124125
"@types/semver": "^7.5.8",
@@ -166,5 +167,8 @@
166167
"vue-tippy": "^6.4.4",
167168
"vue-tsc": "^2.0.26",
168169
"ws": "^8.17.0"
170+
},
171+
"dependencies": {
172+
"js-yaml": "^4.1.0"
169173
}
170174
}

src/main/client/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ipcMain, Notification } from 'electron'
22
import { LocalClient } from './local'
33
import { SSHClient } from './ssh'
44
import { Client } from './client.interface'
5+
import { VaporClient } from './vapor'
56
import DockerClient from './docker'
67
import KubectlClient from './kubectl'
78

@@ -107,6 +108,10 @@ const getClient = (data: any): Client => {
107108
return new DockerClient(data.connection)
108109
}
109110

111+
if (data.connection.type === 'vapor') {
112+
return new VaporClient(data.connection)
113+
}
114+
110115
if (data.connection.type === 'ssh') {
111116
return new SSHClient(data.connection)
112117
}

src/main/client/vapor.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { BaseClient } from './client.base'
2+
import { readFileSync } from 'fs'
3+
import { load as yamlParse } from 'js-yaml'
4+
import { exec } from 'child_process'
5+
import { promisify } from 'util'
6+
import path from 'path'
7+
import { ConnectionConfig } from '../../types/vapor.type'
8+
9+
const execAsync = promisify(exec)
10+
11+
export class VaporClient extends BaseClient {
12+
protected blackListToken = ['<?php', '?>']
13+
14+
constructor(public connection: ConnectionConfig) {
15+
super(connection)
16+
}
17+
18+
async execute(code: string, loader?: string): Promise<string> {
19+
const clientPath = this.connection.client_path
20+
const env = this.connection.environment || 'local'
21+
22+
if (!clientPath) throw new Error('Missing client path in connection configuration.')
23+
24+
if (loader) {
25+
return 'The loader option is not supported in connection type vapor.'
26+
}
27+
28+
const codeCleaned = this.removeBlacklistedTokens(code)
29+
const encoded = Buffer.from(codeCleaned).toString('base64')
30+
const command = `vapor tinker ${env} -n --code 'eval(base64_decode("${encoded}"));'`
31+
32+
try {
33+
const { stdout, stderr } = await execAsync(command, { cwd: clientPath })
34+
35+
if (stderr) {
36+
return `Error: ${stderr}`
37+
}
38+
39+
return stdout
40+
} catch (err) {
41+
return `Exception: ${(err as Error).message}`
42+
}
43+
}
44+
45+
async info(loader?: string): Promise<string> {
46+
return new Promise(async resolve => {
47+
resolve('{}')
48+
})
49+
}
50+
51+
/**
52+
* Get the list of environments defined in the vapor.yml file.
53+
*/
54+
getEnvironmentsAction(): string[] {
55+
const clientPath = this.connection.client_path
56+
if (!clientPath) return []
57+
58+
const vaporPath = path.join(clientPath, 'vapor.yml')
59+
60+
try {
61+
const vaporContent = readFileSync(vaporPath, 'utf8')
62+
const parsed = yamlParse(vaporContent)
63+
64+
if (parsed && typeof parsed === 'object' && 'environments' in parsed) {
65+
return Object.keys((parsed as any).environments || {})
66+
}
67+
68+
return []
69+
} catch (error) {
70+
return []
71+
}
72+
}
73+
74+
/**
75+
* Remove blacklisted tokens from the code.
76+
*/
77+
private removeBlacklistedTokens(code: string): string {
78+
this.blackListToken.forEach(token => {
79+
if (code.includes(token)) {
80+
code = code.replace(token, '')
81+
}
82+
})
83+
return code
84+
}
85+
}

src/renderer/components/TitleBar.vue

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,35 @@
77
import VerticalSplitIcon from './icons/VerticalSplitIcon.vue'
88
import { useSettingsStore } from '../stores/settings'
99
import { useExecuteStore } from '../stores/execute'
10+
import { useVaporStore } from '../stores/vapor'
1011
import Toolbar from './Toolbar.vue'
1112
import { useTabsStore } from '../stores/tabs'
1213
import SecondaryButton from './SecondaryButton.vue'
13-
import { computed, ComputedRef } from 'vue'
14+
import { computed, ComputedRef, watch } from 'vue'
1415
import { Tab } from '../../types/tab.type'
1516
1617
const settingsStore = useSettingsStore()
1718
const executeStore = useExecuteStore()
1819
const tabStore = useTabsStore()
20+
const vaporStore = useVaporStore()
1921
const route = useRoute()
2022
const platform = window.platformInfo.getPlatform()
2123
const tab: ComputedRef<Tab | null> = computed(() => tabStore.getCurrent())
2224
25+
const showOutputType = computed(() => {
26+
return tab.value?.execution !== 'vapor'
27+
})
28+
29+
watch(
30+
() => tab.value?.execution,
31+
value => {
32+
if (value === 'vapor') {
33+
settingsStore.settings.output = 'code'
34+
settingsStore.update()
35+
}
36+
}
37+
)
38+
2339
const execute = () => {
2440
if (route.name !== 'code') {
2541
router.push({ name: 'home' })
@@ -37,6 +53,11 @@
3753
settingsStore.settings.output = output
3854
settingsStore.update()
3955
}
56+
57+
const removeTab = (id: number) => {
58+
tabStore.removeTab(id)
59+
vaporStore.removeVaporConfig(id)
60+
}
4061
</script>
4162

4263
<template>
@@ -75,6 +96,7 @@
7596
/>
7697
</SecondaryButton>
7798
<SecondaryButton
99+
v-if="showOutputType"
78100
class="!px-2"
79101
v-tippy="{ content: 'Output Style', placement: 'bottom' }"
80102
@click="updateOutput(settingsStore.settings.output === 'stack' ? 'code' : 'stack')"
@@ -101,7 +123,7 @@
101123
v-if="tab"
102124
class="!px-2"
103125
v-tippy="{ content: 'Close', placement: 'bottom' }"
104-
@click="tabStore.removeTab(tab.id)"
126+
@click="removeTab(tab.id)"
105127
>
106128
<XMarkIcon class="size-4" />
107129
</SecondaryButton>

src/renderer/components/Toolbar.vue

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import SecondaryButton from './SecondaryButton.vue'
44
import DockerIcon from './icons/DockerIcon.vue'
55
import KubectlIcon from './icons/KubectlIcon.vue'
6+
import VaporIcon from './icons/VaporIcon.vue'
67
import { useTabsStore } from '../stores/tabs'
78
import DropDown from './DropDown.vue'
89
import DropDownItem from './DropDownItem.vue'
@@ -20,6 +21,7 @@
2021
import KubectlView from '../views/KubectlView.vue'
2122
import { ConnectReply } from '../../types/client.type'
2223
import { useLodaersStore } from '../stores/loaders'
24+
import { useVaporStore } from '../stores/vapor.ts'
2325
import Divider from './Divider.vue'
2426
import { useRouter } from 'vue-router'
2527
@@ -28,6 +30,7 @@
2830
const sshStore = useSSHStore()
2931
const kubectlStore = useKubectlStore()
3032
const loadersStore = useLodaersStore()
33+
const vaporStore = useVaporStore()
3134
const router = useRouter()
3235
const dockerModal = ref()
3336
const sshModal = ref()
@@ -49,6 +52,11 @@
4952
if (!tabStore.current) {
5053
return
5154
}
55+
56+
if (execution !== 'vapor') {
57+
vaporStore.removeEnvironment(tabStore.current.id)
58+
}
59+
5260
connecting.value = execution
5361
let connection = tabStore.getConnectionConfig(tabStore.current, execution)
5462
window.ipcRenderer.send('client.connect', {
@@ -102,6 +110,34 @@
102110
}
103111
}
104112
113+
const vaporConfig = computed(() => vaporStore.getConnectionConfig(tabStore?.current?.id))
114+
115+
const vaporConnected = (environment: string) => {
116+
if (!tabStore.current) {
117+
return
118+
}
119+
120+
tabStore.current.execution = 'vapor'
121+
tabStore.updateTab(tabStore.current)
122+
if (tab.value?.id) {
123+
vaporStore.setEnvironment(tabStore.current.id, environment)
124+
}
125+
}
126+
127+
const vaporRemoved = () => {
128+
if (!tabStore.current) {
129+
return
130+
}
131+
132+
tabStore.current.execution = 'local'
133+
vaporStore.removeEnvironment(tabStore.current.id)
134+
tabStore.updateTab(tabStore.current)
135+
}
136+
137+
function capitalize(str: string) {
138+
return str.charAt(0).toUpperCase() + str.slice(1)
139+
}
140+
105141
const kubectlConnected = (config: KubectlConnectionConfig) => {
106142
if (!tabStore.current) {
107143
return
@@ -187,6 +223,33 @@
187223
</div>
188224
</DropDown>
189225

226+
<!-- vapor -->
227+
<DropDown>
228+
<template v-slot:trigger>
229+
<SecondaryButton class="!px-2">
230+
<VaporIcon
231+
class="size-4 mr-1"
232+
:class="{ '!text-[#25C4F2]': tabStore.getCurrent()?.execution === 'vapor' }"
233+
/>
234+
<span class="text-xs max-w-[150px] truncate flex items-center gap-2">
235+
<span v-if="vaporConfig?.environment">
236+
{{ capitalize(vaporConfig.environment) }}
237+
</span>
238+
<span v-else>Vapor</span>
239+
</span>
240+
<ChevronDownIcon class="size-4 ml-1" />
241+
</SecondaryButton>
242+
</template>
243+
<div>
244+
<template v-for="env in vaporConfig?.environments" :key="env">
245+
<DropDownItem v-if="vaporConfig?.environment !== env" @click="vaporConnected(env)" class="truncate">
246+
{{ capitalize(env) }}
247+
</DropDownItem>
248+
</template>
249+
<DropDownItem v-if="vaporConfig?.environment" @click="vaporRemoved()"> Disconnect </DropDownItem>
250+
</div>
251+
</DropDown>
252+
190253
<!-- ssh -->
191254
<DropDown>
192255
<template v-slot:trigger>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<template>
2+
<svg viewBox="0 0 40 40" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
3+
<path
4+
fill-rule="evenodd"
5+
clip-rule="evenodd"
6+
d="M4.345 9h10.55L9.618 20 4.345 9zm21.099 0h10.55l-5.276 11-5.274-11z"
7+
fill="currentColor"
8+
fill-opacity=".1"
9+
></path>
10+
<path
11+
fill-rule="evenodd"
12+
clip-rule="evenodd"
13+
d="M9.62 20h10.549l-5.275 11L9.62 20z"
14+
fill="currentColor"
15+
fill-opacity=".22"
16+
></path>
17+
<path
18+
fill-rule="evenodd"
19+
clip-rule="evenodd"
20+
d="M20.169 20h10.55l-5.275 11-5.275-11z"
21+
fill="currentColor"
22+
fill-opacity=".2"
23+
></path>
24+
<path
25+
fill-rule="evenodd"
26+
clip-rule="evenodd"
27+
d="M20.169 20H9.619l5.275-11 5.275 11z"
28+
fill="currentColor"
29+
fill-opacity=".4"
30+
></path>
31+
<path
32+
fill-rule="evenodd"
33+
clip-rule="evenodd"
34+
d="M30.718 20h-10.55l5.276-11 5.274 11z"
35+
fill="currentColor"
36+
fill-opacity=".4"
37+
></path>
38+
<path
39+
fill-rule="evenodd"
40+
clip-rule="evenodd"
41+
d="M25.444 31h-10.55l5.275-11 5.275 11z"
42+
fill="currentColor"
43+
fill-opacity=".5"
44+
></path>
45+
<path
46+
fill-rule="evenodd"
47+
clip-rule="evenodd"
48+
d="M3.494 8.467A1 1 0 0 1 4.34 8h10.55a1 1 0 0 1 .902.568l4.373 9.12 4.373-9.12A1 1 0 0 1 25.44 8h10.55a1 1 0 0 1 .902 1.432L26.345 31.424a1.001 1.001 0 0 1-.905.576H14.89a1 1 0 0 1-.902-.568l-10.55-22a1 1 0 0 1 .056-.965zm21.95 2.846L29.13 19h-7.372l3.686-7.687zM5.934 10l3.686 7.687L13.306 10H5.933zm8.96 1.313L18.58 19h-7.372l3.686-7.687zM27.032 10l3.686 7.687L34.405 10h-7.373zm-1.588 18.687L21.758 21h7.372l-3.686 7.687zM23.855 30l-3.686-7.687L16.483 30h7.372zm-8.96-1.313L11.207 21h7.372l-3.686 7.687z"
49+
fill="currentColor"
50+
></path>
51+
</svg>
52+
</template>

0 commit comments

Comments
 (0)