Skip to content

Commit d843a47

Browse files
authored
Refine install builder (hestiacp#4059)
1 parent 9fa78bc commit d843a47

File tree

8 files changed

+418
-342
lines changed

8 files changed

+418
-342
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<template>
2+
<div class="CopyToClipboardInput" v-bind="$attrs">
3+
<input type="text" class="CopyToClipboardInput-input" readonly :value="value" />
4+
<button
5+
type="button"
6+
class="CopyToClipboardInput-button"
7+
@click="copyToClipboard"
8+
title="Copy to Clipboard"
9+
>
10+
Copy
11+
</button>
12+
</div>
13+
</template>
14+
15+
<script setup>
16+
import { ref } from "vue";
17+
18+
const { value } = defineProps({
19+
value: {
20+
type: String,
21+
required: true,
22+
},
23+
});
24+
25+
const copyToClipboard = (event) => {
26+
navigator.clipboard.writeText(value).then(
27+
() => {
28+
event.target.textContent = "Copied!";
29+
setTimeout(() => {
30+
event.target.textContent = "Copy";
31+
}, 1000);
32+
},
33+
(err) => {
34+
console.error("Could not copy to clipboard:", err);
35+
},
36+
);
37+
};
38+
</script>
39+
40+
<style scoped>
41+
.CopyToClipboardInput {
42+
position: relative;
43+
}
44+
.CopyToClipboardInput-input {
45+
font-size: 0.9em;
46+
font-family: monospace;
47+
border: 1px solid var(--vp-c-border);
48+
border-radius: 4px;
49+
background-color: var(--vp-c-bg);
50+
width: 100%;
51+
padding: 8px 13px;
52+
padding-right: 53px;
53+
54+
&:hover {
55+
border-color: var(--vp-c-border-hover);
56+
}
57+
58+
&:focus {
59+
border-color: var(--vp-c-brand);
60+
}
61+
}
62+
.CopyToClipboardInput-button {
63+
position: absolute;
64+
top: 1px;
65+
right: 1px;
66+
bottom: 1px;
67+
border-top-right-radius: 3px;
68+
border-bottom-right-radius: 3px;
69+
color: var(--vp-c-brand);
70+
font-weight: 600;
71+
padding: 6px 10px;
72+
background-color: var(--vp-c-bg);
73+
}
74+
</style>
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
<template>
2+
<div class="InstallBuilder">
3+
<div class="container">
4+
<div class="output-card">
5+
<h2 class="u-text-center">Installation instructions</h2>
6+
<p class="u-mb10">
7+
Log in to your server e.g.
8+
<code>ssh root@your.server</code> and download the installation script:
9+
</p>
10+
<CopyToClipboardInput
11+
class="u-mb10"
12+
value="wget https://raw.githubusercontent.com/hestiacp/hestiacp/release/install/hst-install.sh"
13+
/>
14+
<p class="u-mb10">
15+
Check you are running as the <code>root</code> user, configure the options you want below,
16+
then run:
17+
</p>
18+
<CopyToClipboardInput class="u-mb10" :value="installCommand" />
19+
</div>
20+
<h2 class="u-text-center">Configure options</h2>
21+
<ul class="option-list">
22+
<li
23+
v-for="option in options"
24+
:key="option.flag"
25+
:class="{
26+
'option-item': true,
27+
'is-active': selectedOptions[option.flag].enabled,
28+
'is-clickable': !option.type || !selectedOptions[option.flag].enabled,
29+
}"
30+
@click="toggleOption(option)"
31+
>
32+
<div class="option-header">
33+
<div class="form-check">
34+
<input
35+
type="checkbox"
36+
class="form-check-input"
37+
:id="option.flag"
38+
v-model="selectedOptions[option.flag].enabled"
39+
/>
40+
<label :for="option.flag" @click.stop>{{ option.label }}</label>
41+
</div>
42+
<div class="option-icon" v-tooltip="option.description">
43+
<i class="fa-solid fa-circle-info"></i>
44+
</div>
45+
</div>
46+
<div v-if="selectedOptions[option.flag].enabled && option.type" class="option-content">
47+
<label
48+
v-if="option.type && option.type !== 'checkbox'"
49+
class="form-label"
50+
:for="`${option.flag}-input`"
51+
>
52+
{{ option.description }}
53+
</label>
54+
<input
55+
v-if="option.type === 'text'"
56+
class="form-control"
57+
type="text"
58+
:id="`${option.flag}-input`"
59+
v-model="selectedOptions[option.flag].value"
60+
/>
61+
<select
62+
v-if="option.type === 'select'"
63+
class="form-select"
64+
:id="`${option.flag}-input`"
65+
v-model="selectedOptions[option.flag].value"
66+
>
67+
<option v-for="opt in option.options" :key="opt.value" :value="opt.value">
68+
{{ opt.label }}
69+
</option>
70+
</select>
71+
</div>
72+
</li>
73+
</ul>
74+
</div>
75+
</div>
76+
</template>
77+
78+
<script setup>
79+
import { ref, watchEffect } from "vue";
80+
import CopyToClipboardInput from "./CopyToClipboardInput.vue";
81+
import FloatingVue from "floating-vue";
82+
83+
const { options } = defineProps({
84+
options: {
85+
type: Array,
86+
required: true,
87+
},
88+
});
89+
90+
// Initialize selectedOptions with default values
91+
const selectedOptions = ref({});
92+
options.forEach((option) => {
93+
selectedOptions.value[option.flag] = {
94+
enabled: option.default === "yes",
95+
value: option.default !== "yes" && option.default !== "no" ? option.default : null,
96+
};
97+
});
98+
99+
// Handle clicking the entire option "card"
100+
const toggleOption = (option) => {
101+
// Only toggle if option is a standard checkbox, or the option is unchecked
102+
if (!option.type || !selectedOptions.value[option.flag].enabled) {
103+
selectedOptions.value[option.flag].enabled = !selectedOptions.value[option.flag].enabled;
104+
}
105+
};
106+
107+
// Build the install command
108+
const installCommand = ref("bash hst-install.sh");
109+
watchEffect(() => {
110+
let cmd = "bash hst-install.sh";
111+
for (const [key, { enabled, value }] of Object.entries(selectedOptions.value)) {
112+
const opt = options.find((o) => o.flag === key);
113+
114+
if (!opt.type || opt.type === "checkbox") {
115+
if (enabled !== (opt.default === "yes")) {
116+
cmd += ` --${key}=${enabled ? "yes" : "no"}`;
117+
}
118+
} else if (enabled && value !== opt.default) {
119+
cmd += ` --${key}=${value}`;
120+
}
121+
}
122+
installCommand.value = cmd;
123+
});
124+
</script>
125+
126+
<style scoped>
127+
.InstallBuilder {
128+
padding: 0 24px;
129+
130+
@media (min-width: 640px) {
131+
padding: 0 48px;
132+
}
133+
134+
@media (min-width: 960px) {
135+
padding: 0 72px;
136+
}
137+
}
138+
h2 {
139+
font-size: 24px;
140+
font-weight: 600;
141+
margin-bottom: 25px;
142+
}
143+
.container {
144+
display: flex;
145+
flex-direction: column;
146+
margin: 0 auto;
147+
max-width: 1152px;
148+
}
149+
.output-card {
150+
background-color: var(--vp-c-bg-alt);
151+
border-radius: 10px;
152+
padding: 30px 40px;
153+
margin-top: 40px;
154+
margin-bottom: 40px;
155+
}
156+
.option-list {
157+
display: grid;
158+
grid-gap: 20px;
159+
margin-bottom: 50px;
160+
161+
@media (min-width: 640px) {
162+
grid-template-columns: 1fr 1fr;
163+
}
164+
165+
@media (min-width: 960px) {
166+
grid-template-columns: 1fr 1fr 1fr;
167+
}
168+
}
169+
.option-item {
170+
font-size: 0.9em;
171+
border-radius: 10px;
172+
border: 2px solid transparent;
173+
padding: 10px 20px;
174+
background-color: var(--vp-c-bg-alt);
175+
transition: border-color 0.2s;
176+
177+
&:hover {
178+
border-color: var(--vp-button-brand-hover-bg);
179+
}
180+
181+
&.is-active {
182+
border-color: var(--vp-button-brand-active-bg);
183+
}
184+
}
185+
.option-header {
186+
display: flex;
187+
align-items: center;
188+
justify-content: space-between;
189+
}
190+
.option-icon {
191+
padding: 5px 0 5px 10px;
192+
margin-left: 5px;
193+
194+
& i {
195+
opacity: 0.7;
196+
}
197+
198+
&:hover i {
199+
opacity: 1;
200+
}
201+
}
202+
.option-content {
203+
margin-top: 5px;
204+
margin-bottom: 5px;
205+
}
206+
.form-label {
207+
display: inline-block;
208+
padding-bottom: 5px;
209+
}
210+
.form-control {
211+
font-size: 0.9em;
212+
border: 1px solid var(--vp-c-border);
213+
border-radius: 4px;
214+
background-color: var(--vp-c-bg);
215+
width: 100%;
216+
padding: 5px 10px;
217+
218+
&:hover {
219+
border-color: var(--vp-c-border-hover);
220+
}
221+
222+
&:focus {
223+
border-color: var(--vp-c-brand);
224+
}
225+
}
226+
.form-select {
227+
appearance: auto;
228+
font-size: 0.9em;
229+
border: 1px solid var(--vp-c-border);
230+
border-radius: 4px;
231+
background-color: var(--vp-c-bg);
232+
padding: 6px;
233+
width: 100%;
234+
235+
&:hover {
236+
border-color: var(--vp-c-border-hover);
237+
}
238+
239+
&:focus {
240+
border-color: var(--vp-c-brand);
241+
}
242+
}
243+
.form-check {
244+
flex-grow: 1;
245+
position: relative;
246+
padding-left: 25px;
247+
248+
& label {
249+
font-size: 16px;
250+
font-weight: 600;
251+
display: block;
252+
line-height: 1.6;
253+
254+
&:hover {
255+
cursor: pointer;
256+
}
257+
}
258+
}
259+
.form-check-input {
260+
cursor: pointer;
261+
position: absolute;
262+
width: 15px;
263+
height: 15px;
264+
margin-top: 5px;
265+
margin-left: -25px;
266+
}
267+
.u-mb10 {
268+
margin-bottom: 10px !important;
269+
}
270+
.u-text-center {
271+
text-align: center !important;
272+
}
273+
.is-clickable {
274+
cursor: pointer;
275+
}
276+
</style>

0 commit comments

Comments
 (0)