Просмотр исходного кода

Add install script generator (#3355)

* Add support for an install generator

* Update colors

* Add select box for languages

* Tidy install generator UI a bit

* Tidy

* Use dialog to display install string

* Add dependencies + Conflicts

VSFTP + Proftpd and  Mysql And Mysql8 can't go together
- Sieve requires Dovecot
- Dovecot requires Exim and so on

* Update Teams page

* Remove exta code

* Refine install generator

* Improve install modal

* Simplify install generator code

---------

Co-authored-by: Alec Rust <me@alecrust.com>
Jaap Marcus 2 лет назад
Родитель
Сommit
2fbf4b44e3

+ 1 - 0
docs/.vitepress/config.ts

@@ -54,6 +54,7 @@ export default defineConfig({
 function nav(): DefaultTheme.NavItem[] {
 function nav(): DefaultTheme.NavItem[] {
 	return [
 	return [
 		{ text: "Features", link: "/features.md" },
 		{ text: "Features", link: "/features.md" },
+		{ text: "Install", link: "/install.md" },
 		{ text: "Documentation", link: "/docs/introduction/getting-started.md", activeMatch: "/docs/" },
 		{ text: "Documentation", link: "/docs/introduction/getting-started.md", activeMatch: "/docs/" },
 		{ text: "Team", link: "/team.md" },
 		{ text: "Team", link: "/team.md" },
 		{ text: "Demo", link: "https://demo.hestiacp.com:8083/" },
 		{ text: "Demo", link: "https://demo.hestiacp.com:8083/" },

+ 324 - 0
docs/.vitepress/theme/components/InstallOptions.vue

@@ -0,0 +1,324 @@
+<script lang="ts">
+import { InstallOptions } from "../../../_data/options";
+import { LanguagesOptions } from "../../../_data/languages";
+import { ref } from "vue";
+const slot = ref(null);
+
+export default {
+	props: {
+		languages: {
+			type: Array<LanguagesOptions>,
+			required: true,
+			selected: "en",
+		},
+		items: {
+			type: Array<InstallOptions>,
+			required: true,
+		},
+	},
+	data() {
+		return {
+			pageloader: false,
+			hestia_wget:
+				"wget https://raw.githubusercontent.com/hestiacp/hestiacp/release/install/hst-install.sh",
+			hestia_install: "sudo bash hst-install.sh",
+			installStr: "",
+		};
+	},
+	methods: {
+		getOptionString(item: InstallOptions): string {
+			if (item.textField && item.selected) {
+				return item.text.length >= 2 ? `${item.param} '${item.text}'` : "";
+			}
+
+			if (item.selectField) {
+				return `${item.param} '${item.text}'`;
+			}
+
+			return item.param.includes("force") && item.selected
+				? item.param
+				: `${item.param}${item.selected ? " yes" : " no"}`;
+		},
+		generateString() {
+			const installStr = this.items.map(this.getOptionString).filter(Boolean);
+
+			this.installStr = `${this.hestia_install} ${installStr.join(" ")}`;
+			(this.$refs.dialog as HTMLDialogElement).showModal();
+		},
+		closeDialog(e) {
+			if (e.target === this.$refs.dialogClose || e.target === this.$refs.dialog) {
+				(this.$refs.dialog as HTMLDialogElement).close();
+			}
+		},
+		toggleOption(e) {
+			if (e.target.checked) {
+				let conflicts = e.target.getAttribute("conflicts");
+				if (conflicts) {
+					document.getElementById(conflicts).checked = false;
+				}
+				let depends = e.target.getAttribute("depends");
+				if (depends) {
+					document.getElementById(depends).checked = true;
+				}
+			}
+		},
+		copyToClipboard(text: string, button: HTMLButtonElement) {
+			navigator.clipboard.writeText(text).then(
+				() => {
+					button.textContent = "Copied!";
+					setTimeout(() => {
+						button.textContent = "Copy";
+					}, 1000);
+				},
+				(err) => {
+					console.error("Could not copy to clipboard:", err);
+				}
+			);
+		},
+	},
+};
+</script>
+
+<template>
+	<div class="container">
+		<div class="grid">
+			<div class="form-group" v-for="item in items">
+				<div class="form-check u-mb10">
+					<input
+						@change="toggleOption"
+						type="checkbox"
+						class="form-check-input"
+						v-model="item.selected"
+						:value="item.value"
+						:id="item.id"
+						:conflicts="item.conflicts"
+						:depends="item.depends"
+					/>
+					<label :for="item.id">{{ item.id }}</label>
+				</div>
+				<template v-if="item.textField || item.selectField">
+					<label class="form-label" :for="'input-' + item.id">{{ item.desc }}</label>
+				</template>
+				<template v-else>
+					<p>{{ item.desc }}</p>
+				</template>
+				<div v-if="item.textField">
+					<input type="text" class="form-control" v-model="item.text" :id="'input-' + item.id" />
+				</div>
+				<div v-if="item.selectField">
+					<select class="form-select" v-model="item.text" :id="'input-' + item.id">
+						<option v-for="language in languages" :value="language.value" :key="language.value">
+							{{ language.text }}
+						</option>
+					</select>
+				</div>
+			</div>
+		</div>
+		<div class="u-text-center u-mb10">
+			<button @click="generateString" class="form-submit" type="button">Submit</button>
+		</div>
+		<dialog ref="dialog" class="modal" @click="closeDialog">
+			<button class="modal-close" @click="closeDialog" type="button" ref="dialogClose">
+				Close
+			</button>
+			<div ref="dialogContent" class="modal-content">
+				<h1 class="modal-heading">Installation instructions</h1>
+				<p class="u-mb10">
+					Log in to your server as root, either directly or via SSH:
+					<code>ssh root@your.server</code> and download the installation script:
+				</p>
+				<div class="u-pos-relative">
+					<input
+						type="text"
+						class="form-control u-monospace u-mb10"
+						v-model="hestia_wget"
+						readonly
+					/>
+					<button
+						class="button-positioned"
+						@click="copyToClipboard(hestia_wget, $event.target)"
+						type="button"
+						title="Copy to Clipboard"
+					>
+						Copy
+					</button>
+				</div>
+				<p class="u-mb10">Then run the following command:</p>
+				<div class="u-pos-relative">
+					<textarea class="form-control u-min-height100" v-model="installStr" readonly />
+					<button
+						class="button-positioned"
+						@click="copyToClipboard(installStr, $event.target)"
+						type="button"
+						title="Copy to Clipboard"
+					>
+						Copy
+					</button>
+				</div>
+			</div>
+		</dialog>
+	</div>
+</template>
+
+<style scoped>
+.container {
+	margin: 0px auto;
+	max-width: 1152px;
+}
+.grid {
+	display: grid;
+	grid-gap: 20px;
+	margin-top: 30px;
+	margin-bottom: 30px;
+
+	@media (min-width: 640px) {
+		grid-template-columns: 1fr 1fr;
+	}
+
+	@media (min-width: 960px) {
+		grid-template-columns: 1fr 1fr 1fr;
+	}
+}
+.form-group {
+	font-size: 0.9em;
+	border-radius: 10px;
+	padding: 15px 20px;
+	background-color: var(--vp-c-bg-alt);
+}
+.form-label {
+	display: inline-block;
+	margin-left: 2px;
+	padding-bottom: 5px;
+	text-transform: capitalize;
+}
+.form-control {
+	font-size: 0.9em;
+	border: 1px solid var(--vp-c-border);
+	border-radius: 4px;
+	background-color: var(--vp-c-bg);
+	width: 100%;
+	padding: 5px 10px;
+
+	&:hover {
+		border-color: var(--vp-c-border-hover);
+	}
+
+	&:focus {
+		border-color: var(--vp-c-brand);
+	}
+}
+.form-select {
+	appearance: auto;
+	font-size: 0.9em;
+	border: 1px solid var(--vp-c-border);
+	border-radius: 4px;
+	background-color: var(--vp-c-bg);
+	padding: 5px 10px;
+	width: 100%;
+
+	&:hover {
+		border-color: var(--vp-c-border-hover);
+	}
+
+	&:focus {
+		border-color: var(--vp-c-brand);
+	}
+}
+.form-check {
+	position: relative;
+	padding-left: 20px;
+	margin-left: 3px;
+	min-height: 24px;
+
+	& label {
+		font-weight: 600;
+	}
+}
+.form-check-input {
+	position: absolute;
+	margin-top: 5px;
+	margin-left: -20px;
+}
+.form-submit {
+	border: 1px solid transparent;
+	display: inline-block;
+	font-weight: 600;
+	transition: color 0.25s, border-color 0.25s, background-color 0.25s;
+	border-radius: 20px;
+	font-size: 16px;
+	padding: 10px 20px;
+	background-color: var(--vp-button-brand-bg);
+	border-color: var(--vp-button-brand-border);
+	color: var(--vp-button-brand-text);
+
+	&:hover {
+		background-color: var(--vp-button-brand-hover-bg);
+		border-color: var(--vp-button-brand-hover-border);
+		color: var(--vp-button-brand-hover-text);
+	}
+
+	&:active {
+		background-color: var(--vp-button-brand-active-bg);
+		border-color: var(--vp-button-brand-active-border);
+		color: var(--vp-button-brand-active-text);
+	}
+}
+.button-positioned {
+	position: absolute;
+	right: 1px;
+	top: 1px;
+	border-top-right-radius: 3px;
+	border-bottom-right-radius: 3px;
+	color: var(--vp-c-brand);
+	font-weight: 600;
+	padding: 6px 10px;
+	background-color: var(--vp-c-bg);
+}
+.modal {
+	position: fixed;
+	border-radius: 10px;
+	border: 1px solid var(--vp-c-border);
+	box-shadow: 0 8px 40px 0 rgb(0 0 0 / 35%);
+	padding: 0;
+
+	&::backdrop {
+		background-color: rgb(0 0 0 / 60%);
+	}
+}
+.modal-close {
+	position: absolute;
+	top: 10px;
+	right: 15px;
+	font-weight: 600;
+	color: var(--vp-c-brand);
+}
+.modal-content {
+	padding: 30px;
+}
+.modal-heading {
+	font-weight: 600;
+	font-size: 1.3em;
+	text-align: center;
+	margin-bottom: 15px;
+}
+code {
+	background-color: var(--vp-c-bg-alt);
+	border-radius: 3px;
+	padding: 2px 5px;
+}
+.u-mb10 {
+	margin-bottom: 10px !important;
+}
+.u-min-height100 {
+	min-height: 100px;
+}
+.u-text-center {
+	text-align: center !important;
+}
+.u-monospace {
+	font-family: monospace !important;
+}
+.u-pos-relative {
+	position: relative !important;
+}
+</style>

+ 31 - 0
docs/.vitepress/theme/components/InstallOptionsSection.vue

@@ -0,0 +1,31 @@
+<script lang="ts"></script>
+
+<template>
+	<form class="InstallForm" id="form">
+		<div class="InstallOptionsSection">
+			<slot name="list" />
+		</div>
+		<cite
+			>Based on: <a href="https://github.com/gabizz/hestiacp-scriptline-generator">@gabizz</a> and
+			<a href="https://github.com/turbopixel/HestiaCP-Command-Creator">@turbopixel</a></cite
+		>
+	</form>
+</template>
+
+<style scoped>
+.InstallForm {
+	margin: 0.55em 0;
+	padding: 0 1em;
+	line-height: 1.5;
+}
+cite {
+	font-size: small;
+	margin: 0.55em 0;
+	display: block;
+	text-align: center;
+
+	& a {
+		color: var(--vp-c-txt-1) !important;
+	}
+}
+</style>

+ 28 - 0
docs/.vitepress/theme/components/InstallPage.vue

@@ -0,0 +1,28 @@
+<template>
+	<div class="InstallPage">
+		<slot></slot>
+	</div>
+</template>
+
+<style scoped>
+.InstallPage {
+	line-height: 1.5;
+}
+.InstallPage :deep(.container) {
+	display: flex;
+	flex-direction: column;
+	margin: 0 auto;
+	max-width: 1152px;
+}
+
+.InstallPage :deep(a) {
+	font-weight: 500;
+	color: var(--vp-c-brand);
+	text-decoration-style: dotted;
+	transition: color 0.25s;
+}
+
+.InstallPage :deep(a:hover) {
+	color: var(--vp-c-brand-dark);
+}
+</style>

+ 42 - 0
docs/.vitepress/theme/components/InstallPageTitle.vue

@@ -0,0 +1,42 @@
+<template>
+	<header class="InstallPageTitle">
+		<div class="container">
+			<h1>
+				<slot name="title"></slot>
+			</h1>
+			<p v-if="$slots.lead" class="lead">
+				<slot name="lead" />
+			</p>
+		</div>
+	</header>
+</template>
+
+<style scoped>
+.InstallPageTitle {
+	padding: 0 24px;
+	background-color: var(--vp-c-bg-alt);
+}
+
+.InstallPageTitle h1 {
+	margin: 0.75em 0;
+	font-size: 2rem;
+	font-weight: 700;
+	line-height: inherit;
+}
+
+@media (min-width: 640px) {
+	.InstallPageTitle {
+		padding: 0 48px;
+	}
+
+	.InstallPageTitle h1 {
+		font-size: 2.5rem;
+	}
+}
+
+@media (min-width: 960px) {
+	.InstallPageTitle {
+		padding: 0 64px;
+	}
+}
+</style>

+ 2 - 0
docs/.vitepress/theme/index.ts

@@ -5,10 +5,12 @@ import "@fortawesome/fontawesome-free/css/solid.css";
 import "./styles/base.css";
 import "./styles/base.css";
 import "./styles/vars.css";
 import "./styles/vars.css";
 import FeaturePage from "./components/FeaturePage.vue";
 import FeaturePage from "./components/FeaturePage.vue";
+import InstallPage from "./components/InstallPage.vue";
 
 
 export default {
 export default {
 	...Theme,
 	...Theme,
 	enhanceApp({ app }) {
 	enhanceApp({ app }) {
 		app.component("FeaturePage", FeaturePage);
 		app.component("FeaturePage", FeaturePage);
+		app.component("InstallPage", InstallPage);
 	},
 	},
 };
 };

+ 42 - 0
docs/_data/languages.ts

@@ -0,0 +1,42 @@
+export const languages: LanguagesListItem[] = [
+	{ text: "Arabic", value: "ar" },
+	{ text: "Armenian", value: "hy" },
+	{ text: "Azerbaijani", value: "az" },
+	{ text: "Bengali", value: "bn" },
+	{ text: "Bosnian", value: "bs" },
+	{ text: "Bulgarian", value: "bg" },
+	{ text: "Croatian", value: "hr" },
+	{ text: "Czech", value: "cs" },
+	{ text: "Danish", value: "da" },
+	{ text: "Dutch", value: "nl" },
+	{ text: "English", value: "en" },
+	{ text: "Finnish", value: "fi" },
+	{ text: "French", value: "fr" },
+	{ text: "Georgian", value: "ka" },
+	{ text: "German", value: "de" },
+	{ text: "Greek", value: "el" },
+	{ text: "Hungarian", value: "hu" },
+	{ text: "Indonesian", value: "id" },
+	{ text: "Italian", value: "it" },
+	{ text: "Japanese", value: "ja" },
+	{ text: "Korean", value: "ko" },
+	{ text: "Kurdish Sorani" },
+	{ text: "Norwegain", value: "no" },
+	{ text: "Persian", value: "fa" },
+	{ text: "Polish", value: "pl" },
+	{ text: "Portuguese", value: "pt" },
+	{ text: "Portuguese (Brasil)", value: "pt-br" },
+	{ text: "Romanian", value: "ro" },
+	{ text: "Russian", value: "ru" },
+	{ text: "Serbian", value: "sr" },
+	{ text: "Simplified Chinese (China)", value: "zh-cn" },
+	{ text: "Slovak", value: "sk" },
+	{ text: "Spanish", value: "es" },
+	{ text: "Swedish", value: "sv" },
+	{ text: "Thai", value: "th" },
+	{ text: "Traditional Chinese (Taiwan)", value: "zh-tw" },
+	{ text: "Turkish", value: "tr" },
+	{ text: "Ukrainian", value: "uk" },
+	{ text: "Urdu", value: "ur" },
+	{ text: "Vietnamese", value: "vi" },
+];

+ 154 - 0
docs/_data/options.ts

@@ -0,0 +1,154 @@
+export const options: OptionsListItem[] = [
+	{
+		name: " --port",
+		id: "port",
+		param: "--port",
+		desc: "Change Backend Port",
+		selected: true,
+		text: "8083",
+		textField: true,
+	},
+	{
+		name: " --lang",
+		id: "language",
+		param: "--lang",
+		desc: "ISO 639-1 codes",
+		selected: true,
+		default: "en",
+		selectField: true,
+		text: "en",
+	},
+	{
+		name: " --hostname",
+		id: "hostname",
+		param: "--hostname",
+		desc: "Set hostname",
+		selected: false,
+		text: "",
+		textField: true,
+	},
+	{
+		name: " --email",
+		id: "email",
+		param: "--email",
+		desc: "Set admin email",
+		selected: false,
+		text: "",
+		textField: true,
+	},
+	{
+		name: " --password",
+		id: "password",
+		param: "--password",
+		desc: "Set admin password",
+		selected: false,
+		text: "",
+		textField: true,
+	},
+	{ name: " --apache", id: "apache", param: "--apache", desc: " Install Apache.", selected: true },
+	{ name: " --phpfpm", id: "phpfpm", param: "--phpfpm", desc: "Install PHP-FPM.", selected: true },
+	{
+		name: " --multiphp",
+		id: "multiphp",
+		param: "--multiphp",
+		desc: " Install Multi-PHP.",
+		selected: true,
+	},
+	{
+		name: " --vsftpd",
+		id: "vsftpd",
+		param: "--vsftpd",
+		desc: "Install Vsftpd.",
+		selected: true,
+		conflicts: "proftpd",
+	},
+	{
+		name: " --proftpd",
+		id: "proftpd",
+		param: "--proftpd",
+		desc: "Install ProFTPD.",
+		selected: false,
+		conflicts: "vsftpd",
+	},
+	{ name: " --named", id: "named", param: "--named", desc: "Install Bind.", selected: true },
+	{
+		name: " --mysql",
+		id: "mysql",
+		param: "--mysql",
+		desc: "Install MariaDB.",
+		selected: true,
+		conflicts: "mysql8",
+	},
+	{
+		name: " --mysql-classic",
+		id: "mysql8",
+		param: "--mysql-classic",
+		desc: "Install Mysql8.",
+		selected: false,
+		conflicts: "mysql",
+	},
+	{
+		name: " --postgresql",
+		id: "postgresql",
+		param: "--postgresql",
+		desc: "Install PostgreSQL.",
+		selected: false,
+	},
+	{ name: " --exim", id: "exim", param: "--exim", desc: "Install Exim.", selected: true },
+	{
+		name: " --dovecot",
+		id: "dovecot",
+		param: "--dovecot",
+		desc: "Install Dovecot.",
+		selected: true,
+		depends: "exim",
+	},
+	{
+		name: " --sieve",
+		id: "sieve",
+		param: "--sieve",
+		desc: "Enable Dovecot sieve.",
+		selected: false,
+		depends: "dovecot",
+	},
+	{
+		name: " --clamav",
+		id: "clamav",
+		param: "--clamav",
+		desc: "Install ClamAV.",
+		selected: true,
+		depends: "exim",
+	},
+	{
+		name: " --spamassassin",
+		id: "spamassassin",
+		param: "--spamassassin",
+		desc: "Install SpamAssassin.",
+		selected: true,
+		depends: "exim",
+	},
+	{
+		name: " --iptables",
+		id: "iptables",
+		param: "--iptables",
+		desc: "Install Iptables.",
+		selected: true,
+	},
+	{
+		name: " --fail2ban",
+		id: "fail2ban",
+		param: "--fail2ban",
+		desc: "Install Fail2ban.",
+		selected: true,
+	},
+	{ name: " --quota", id: "quota", param: "--quota", desc: "Filesystem Quota.", selected: false },
+	{ name: " --api", id: "api", param: "--api", desc: "Activate API.", selected: true },
+	{
+		name: " --interactive",
+		id: "interactive",
+		param: "--interactive",
+		desc: "Interactive install.",
+		selected: true,
+	},
+	{ name: " --force", id: "force", param: "--force", desc: "Force installation.", selected: false },
+];

+ 1 - 1
docs/_data/team.ts

@@ -36,7 +36,7 @@ export const teamMembers: DefaultTheme.TeamMember[] = [
 		orgLink: "https://prosomo.com",
 		orgLink: "https://prosomo.com",
 		links: [
 		links: [
 			{ icon: "github", link: "https://github.com/jakobbouchard" },
 			{ icon: "github", link: "https://github.com/jakobbouchard" },
-			{ icon: "linkedin", link: "https://linkedin.com/in/bouchardjakob" },
+			{ icon: "linkedin", link: "https://linkedin.com/in/jakobbouchard" },
 			{
 			{
 				icon: {
 				icon: {
 					svg: '<svg role="img" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><title>Website</title><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" /></svg>',
 					svg: '<svg role="img" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><title>Website</title><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" /></svg>',

+ 23 - 0
docs/install.md

@@ -0,0 +1,23 @@
+---
+layout: page
+title: Install
+---
+
+<script setup lang="ts">
+  import InstallPageTitle from "./.vitepress/theme/components/InstallPageTitle.vue";
+  import InstallOptions from "./.vitepress/theme/components/InstallOptions.vue";
+  import InstallOptionsSection from "./.vitepress/theme/components/InstallOptionsSection.vue";
+  import { options } from "./_data/options";
+  import { languages } from "./_data/languages";
+</script>
+
+<InstallPage>
+  <InstallPageTitle>
+	<template #title>Install</template>
+  </InstallPageTitle>
+  <InstallOptionsSection>
+  	<template #list>
+	  <InstallOptions :items="options" :languages="languages"></InstallOptions>
+	</template>
+  </InstallOptionsSection>
+</InstallPage>